205 lines
7.1 KiB
Markdown
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).
|