cardano-api/README.md
2026-06-28 21:13:33 -07:00

205 lines
7.1 KiB
Markdown

# cardano-api
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 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).
## Features
- Address balances, native-token holdings, and transaction history from db-sync
- Block, transaction, asset, and stake-pool lookups
- Live UTxO and protocol-parameter queries via the node
- Transaction submission to the network
- 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
## 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
```
The API listens on port `8765`. Put it behind your own reverse proxy / TLS
terminator as needed.
To run without Docker:
```
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 |
| standard | ≥ 50 | 100 | 2 | yes | no |
| elevated | ≥ 500 | 1000 | 10 | yes | yes |
| master | n/a | unlimited | unlimited | yes | yes |
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`.
### Token-gated keys (optional, self-service)
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..." }
-> { "nonce": "...", "expires_at": "..." }
# sign the nonce with the wallet via CIP-8
POST /v1/auth/verify { "address", "nonce", "signature", "key" }
-> { "api_key": "capi_...", "tier", "trp_balance" }
POST /v1/auth/refresh (X-API-Key header — self-service only)
-> { "tier", "trp_balance", "expires_at", ... }
```
## Endpoints
```
GET /health
GET /v1/sync/status
GET /v1/block/latest
GET /v1/block/{block_no}
GET /v1/address/{address}/balance
GET /v1/address/{address}/tokens?page=&limit=
GET /v1/address/{address}/transactions?page=&limit=&order=
GET /v1/address/{address}/utxos (standard+)
GET /v1/tx/{tx_hash}
POST /v1/tx/submit (elevated+)
GET /v1/asset/{policy_id}/info?page=&limit=
GET /v1/asset/{policy_id}/{asset_name}/holders?limit=
GET /v1/pool/{pool_id}/info
GET /v1/protocol-params (standard+)
POST /v1/auth/challenge
POST /v1/auth/verify
POST /v1/auth/refresh
POST /admin/keys (master)
DELETE /admin/keys/{key} (master)
GET /admin/keys (master)
POST /admin/refresh-tiers (master)
GET /admin/stats (master)
```
## Caching
Responses are cached in Redis with per-endpoint TTLs (seconds):
```
balance 60 asset_info 120
tokens 60 pool_info 120
transactions 30 sync_status 5
block_latest 10 protocol_params 300
tx_details 300 utxos 10
```
## Security notes
- 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.
## Contributing
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.
## License
Released under the GNU Affero General Public License v3.0 or later
(AGPL-3.0-or-later). See [LICENSE](LICENSE).