docs: rewrite README for public release + add MIT LICENSE
This commit is contained in:
parent
5344541f79
commit
449d403d3b
3 changed files with 156 additions and 87 deletions
|
|
@ -1,16 +1,8 @@
|
||||||
# .forgejo/workflows/gitleaks.yml
|
# Gitleaks secret-scanning workflow.
|
||||||
#
|
#
|
||||||
# Gitleaks CI workflow for secret scanning. Use it as a CI secret-scan step, e.g. at
|
# Scans the repository for committed secrets on every push and pull request,
|
||||||
# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered
|
# so credentials never land in history unnoticed. It runs on a Forgejo/Gitea
|
||||||
# (after a CI runner is configured).
|
# Actions runner; copy it to .github/workflows/ to run it on GitHub Actions too.
|
||||||
#
|
|
||||||
# Pairs with the server-side pre-receive hook — that one is
|
|
||||||
# the strict enforcement layer (rejects the push); this one provides the
|
|
||||||
# per-PR red ✗ that branch-protection rules can require before merge.
|
|
||||||
#
|
|
||||||
# Layer 1 (this workflow): visible per-PR status, can be a required check.
|
|
||||||
# Layer 2 (pre-receive hook): strict enforcement at the server.
|
|
||||||
# Layer 3 (scheduled cron sweep): nightly full-history sweep across all repos.
|
|
||||||
|
|
||||||
name: gitleaks
|
name: gitleaks
|
||||||
|
|
||||||
|
|
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Sulkta
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
206
README.md
206
README.md
|
|
@ -1,57 +1,138 @@
|
||||||
# cardano-api
|
# cardano-api
|
||||||
|
|
||||||
REST API over cardano-db-sync + cardano-node. FastAPI + asyncpg + Redis.
|
A REST API over [cardano-db-sync](https://github.com/IntersectMBO/cardano-db-sync)
|
||||||
|
and [cardano-node](https://github.com/IntersectMBO/cardano-node), built with
|
||||||
|
FastAPI, asyncpg, and Redis.
|
||||||
|
|
||||||
Read paths hit the db-sync Postgres. UTxO queries, protocol params, and tx submit shell out to `cardano-cli` against a local node socket. Auth is TRP token-gated via CIP-8 wallet signatures.
|
Read queries — address balances, native tokens, transaction history, assets,
|
||||||
|
stake pools, and blocks — are served directly from the db-sync Postgres
|
||||||
|
database. Live UTxO lookups, current protocol parameters, and transaction
|
||||||
|
submission shell out to `cardano-cli` against a local node socket. Access is
|
||||||
|
gated by API keys, and keys can optionally be issued self-service by proving
|
||||||
|
ownership of a wallet that holds a configurable native token (a CIP-8 signature
|
||||||
|
challenge).
|
||||||
|
|
||||||
## Stack
|
## Features
|
||||||
|
|
||||||
- FastAPI / uvicorn
|
- Address balances, native-token holdings, and transaction history from db-sync
|
||||||
- asyncpg → cardano-db-sync Postgres (`cexplorer`)
|
- Block, transaction, asset, and stake-pool lookups
|
||||||
- redis.asyncio → rate limiting + response cache + API-key storage
|
- Live UTxO and protocol-parameter queries via the node
|
||||||
- cardano-cli (baked into the image) → node socket queries + tx submit
|
- Transaction submission to the network
|
||||||
- pycardano + PyNaCl + cbor2 → CIP-8 verification
|
- Redis response caching with per-endpoint TTLs
|
||||||
|
- Tiered API keys with per-tier rate limits
|
||||||
|
- Optional token-gated, self-service key issuance via CIP-8 wallet signatures
|
||||||
|
- Strict input validation (bech32 addresses, hex tx hashes / policy IDs) and a
|
||||||
|
body-size cap on transaction submission
|
||||||
|
|
||||||
## Run
|
## Requirements
|
||||||
|
|
||||||
|
- A synced `cardano-db-sync` Postgres database (the `cexplorer` schema)
|
||||||
|
- A running `cardano-node` with a reachable IPC socket — required only for the
|
||||||
|
node-backed endpoints (live UTxOs, protocol params, tx submit)
|
||||||
|
- Redis — rate limiting, response cache, and API-key storage
|
||||||
|
- `cardano-cli` on `PATH` — the provided Docker image installs it for you
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
The container image bundles `cardano-cli`; you provide the db-sync database, a
|
||||||
|
node socket, and Redis. A minimal `compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
cardano-api:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8765:8765"
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgres-dbsync
|
||||||
|
DB_NAME: cexplorer
|
||||||
|
DB_USER: dbsync
|
||||||
|
DB_PASS: ${DB_PASS}
|
||||||
|
REDIS_HOST: redis
|
||||||
|
API_MASTER_KEY: ${API_MASTER_KEY}
|
||||||
|
CARDANO_NODE_SOCKET_PATH: /node-ipc/node.socket
|
||||||
|
CARDANO_NETWORK: mainnet
|
||||||
|
volumes:
|
||||||
|
- /path/to/node-ipc:/node-ipc
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
```
|
||||||
|
|
||||||
```
|
```
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Listens on `:8765` inside the container. Wire it to whatever proxy / port-forward the deploy wants.
|
The API listens on port `8765`. Put it behind your own reverse proxy / TLS
|
||||||
|
terminator as needed.
|
||||||
|
|
||||||
## Tiers + rate limits
|
To run without Docker:
|
||||||
|
|
||||||
| Tier | TRP needed | Rate (req/min) | tx submit (per min) | Node read | Node submit |
|
```
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn main:app --host 0.0.0.0 --port 8765
|
||||||
|
```
|
||||||
|
|
||||||
|
(Install `cardano-cli` separately if you need the node-backed endpoints.)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All configuration is via environment variables:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `DB_HOST` | `postgres-dbsync` | db-sync Postgres host |
|
||||||
|
| `DB_PORT` | `5432` | db-sync Postgres port |
|
||||||
|
| `DB_NAME` | `cexplorer` | db-sync database name |
|
||||||
|
| `DB_USER` | `dbsync` | db-sync database user |
|
||||||
|
| `DB_PASS` | _(empty)_ | db-sync database password |
|
||||||
|
| `REDIS_HOST` | `redis-api` | Redis host |
|
||||||
|
| `REDIS_PORT` | `6379` | Redis port |
|
||||||
|
| `API_MASTER_KEY` | _(empty)_ | Unrestricted master key for `/admin/*` |
|
||||||
|
| `CARDANO_NODE_SOCKET_PATH` | `/node-ipc/node.socket` | Node IPC socket path |
|
||||||
|
| `CARDANO_NETWORK` | `mainnet` | Cardano network |
|
||||||
|
|
||||||
|
`TRUSTED_PROXIES` in `main.py` controls which client IPs are allowed to set the
|
||||||
|
`X-Forwarded-For` header. It defaults to loopback plus the Docker bridge
|
||||||
|
gateways; override it to match your own proxy setup.
|
||||||
|
|
||||||
|
## Authentication & tiers
|
||||||
|
|
||||||
|
Pass an API key in the `X-API-Key` header (preferred) or as an `?api_key=` query
|
||||||
|
parameter. Requests with no key are treated as anonymous.
|
||||||
|
|
||||||
|
| Tier | Token balance | Rate (req/min) | tx submit (/min) | Node read | Node submit |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| anonymous | 0 | 20 | 0 | no | no |
|
| anonymous | 0 | 20 | 0 | no | no |
|
||||||
| standard | ≥50 | 100 | 2 | yes | no |
|
| standard | ≥ 50 | 100 | 2 | yes | no |
|
||||||
| elevated | ≥500 | 1000 | 10 | yes | yes |
|
| elevated | ≥ 500 | 1000 | 10 | yes | yes |
|
||||||
| master | n/a | unlimited | unlimited | yes | yes |
|
| master | n/a | unlimited | unlimited | yes | yes |
|
||||||
|
|
||||||
Anonymous is rate-limited per source IP. Authed tiers are rate-limited per key.
|
Anonymous requests are rate-limited per source IP; authenticated tiers are
|
||||||
|
rate-limited per key. The master key is supplied out-of-band via `API_MASTER_KEY`
|
||||||
|
and can mint non-expiring keys at `POST /admin/keys`.
|
||||||
|
|
||||||
TRP-gated keys expire 48h after issue and must be re-auth'd. A background task re-checks balances every 10 minutes and re-tiers in place.
|
### Token-gated keys (optional, self-service)
|
||||||
|
|
||||||
## Auth flow (TRP-gated)
|
A wallet that holds enough of a configured native token can mint its own API key
|
||||||
|
by signing a challenge nonce with CIP-8. The gating token's policy ID and the
|
||||||
|
tier thresholds are constants near the top of `main.py` — point them at your own
|
||||||
|
token to use this flow. Token-gated keys expire 48 hours after issue and can be
|
||||||
|
re-authenticated (which also re-tiers them) via `POST /v1/auth/refresh`. A
|
||||||
|
background task re-checks balances every 10 minutes and re-tiers keys in place.
|
||||||
|
|
||||||
```
|
```
|
||||||
POST /v1/auth/challenge { "address": "addr1..." }
|
POST /v1/auth/challenge { "address": "addr1..." }
|
||||||
→ { "nonce": "...", "expires_at": "..." }
|
-> { "nonce": "...", "expires_at": "..." }
|
||||||
|
|
||||||
# sign nonce with the wallet via CIP-8
|
# sign the nonce with the wallet via CIP-8
|
||||||
|
|
||||||
POST /v1/auth/verify { "address", "nonce", "signature", "key" }
|
POST /v1/auth/verify { "address", "nonce", "signature", "key" }
|
||||||
→ { "api_key": "capi_...", "tier", "trp_balance" }
|
-> { "api_key": "capi_...", "tier", "trp_balance" }
|
||||||
|
|
||||||
POST /v1/auth/refresh (X-API-Key header — self-service only)
|
POST /v1/auth/refresh (X-API-Key header — self-service only)
|
||||||
→ { "tier", "trp_balance", "expires_at", ... }
|
-> { "tier", "trp_balance", "expires_at", ... }
|
||||||
```
|
```
|
||||||
|
|
||||||
Master key is issued out-of-band via `API_MASTER_KEY` env. Master-key-created keys (`/admin/keys`) don't expire.
|
|
||||||
|
|
||||||
Header is preferred (`X-API-Key: capi_...`); `?api_key=` works too.
|
|
||||||
|
|
||||||
## Endpoints
|
## Endpoints
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -87,62 +168,37 @@ POST /admin/refresh-tiers (master)
|
||||||
GET /admin/stats (master)
|
GET /admin/stats (master)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cache TTLs (Redis)
|
## Caching
|
||||||
|
|
||||||
|
Responses are cached in Redis with per-endpoint TTLs (seconds):
|
||||||
|
|
||||||
```
|
```
|
||||||
balance 60s
|
balance 60 asset_info 120
|
||||||
tokens 60s
|
tokens 60 pool_info 120
|
||||||
transactions 30s
|
transactions 30 sync_status 5
|
||||||
block_latest 10s
|
block_latest 10 protocol_params 300
|
||||||
tx_details 300s (immutable)
|
tx_details 300 utxos 10
|
||||||
asset_info 120s
|
|
||||||
pool_info 120s
|
|
||||||
sync_status 5s
|
|
||||||
protocol_params 300s
|
|
||||||
utxos 10s
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Validation
|
## Security notes
|
||||||
|
|
||||||
Inputs hit regex gates before any DB query:
|
- API keys are stored as `sha256(key)`. The raw key is returned exactly once, at
|
||||||
|
creation; lookups, listing, and revocation all operate on the hash.
|
||||||
|
- Every path parameter is validated against a strict regex (bech32
|
||||||
|
mainnet/testnet addresses, 64-hex tx hashes, 56-hex policy IDs) before any
|
||||||
|
query runs.
|
||||||
|
- `POST /v1/tx/submit` bodies are capped at 64 KB. The middleware reads the
|
||||||
|
actual request stream, so chunked transfer or a missing `Content-Length`
|
||||||
|
can't bypass the limit.
|
||||||
|
- CIP-8 verification rejects non-EdDSA algorithms, wrong key length, empty or
|
||||||
|
mismatched payloads, and any key that doesn't hash to the claimed address.
|
||||||
|
|
||||||
- bech32 mainnet/testnet addresses (`addr1...` / `addr_test1...`)
|
## Contributing
|
||||||
- 64-hex tx hashes
|
|
||||||
- 56-hex policy IDs
|
|
||||||
|
|
||||||
Tx submit body is capped at 64 KB (middleware reads the actual stream; chunked transfer can't bypass).
|
Issues and pull requests are welcome. Please keep changes focused, run the app
|
||||||
|
against a local db-sync + node before submitting, and don't commit secrets — a
|
||||||
|
gitleaks workflow scans every push and pull request.
|
||||||
|
|
||||||
CIP-8 verification rejects non-EdDSA (`alg ≠ -8`), wrong key length, empty payload, payload-not-nonce, and bad key→address hash binding.
|
## License
|
||||||
|
|
||||||
## Key storage
|
Released under the MIT License. See [LICENSE](LICENSE).
|
||||||
|
|
||||||
Keys are stored as `sha256(key)`. The raw key is returned exactly once at issue. Lookups, admin listing, and revoke all operate on the hash.
|
|
||||||
|
|
||||||
TRP-gated keys are tracked in a `trp_gated_keys` Redis set so the refresh task can batch a single Postgres query for all owner addresses instead of N+1.
|
|
||||||
|
|
||||||
## Known policy IDs
|
|
||||||
|
|
||||||
```
|
|
||||||
TRP 9c4bd4a90cdb73d9ff681215ecf7dea9fb183d916d30487d17098e05
|
|
||||||
MAP 24bd9e7b9ae3a61df79eca72fd8355d0f7767e4c55a04a0d919c019c
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
|
|
||||||
```
|
|
||||||
DB_HOST (default: postgres-dbsync — compose service name)
|
|
||||||
DB_PORT 5432
|
|
||||||
DB_NAME cexplorer
|
|
||||||
DB_USER dbsync
|
|
||||||
DB_PASS
|
|
||||||
|
|
||||||
REDIS_HOST (default: redis-api — compose service name)
|
|
||||||
REDIS_PORT 6379
|
|
||||||
|
|
||||||
API_MASTER_KEY unrestricted-tier key
|
|
||||||
|
|
||||||
CARDANO_NODE_SOCKET_PATH /node-ipc/node.socket
|
|
||||||
CARDANO_NETWORK mainnet
|
|
||||||
```
|
|
||||||
|
|
||||||
`TRUSTED_PROXIES` (in `main.py`) is the set of IPs whose `X-Forwarded-For` header is honoured. Defaults to loopback + the docker default bridges. If the deployment fronts the API with a different proxy, override the set.
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue