# cardano-api REST API over cardano-db-sync + cardano-node. FastAPI + asyncpg + 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. ## Stack - FastAPI / uvicorn - asyncpg → cardano-db-sync Postgres (`cexplorer`) - redis.asyncio → rate limiting + response cache + API-key storage - cardano-cli (baked into the image) → node socket queries + tx submit - pycardano + PyNaCl + cbor2 → CIP-8 verification ## Run ``` docker compose up -d --build ``` Listens on `:8765` inside the container. Wire it to whatever proxy / port-forward the deploy wants. ## Tiers + rate limits | Tier | TRP needed | Rate (req/min) | tx submit (per 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 is rate-limited per source IP. Authed tiers are rate-limited per key. 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. ## Auth flow (TRP-gated) ``` POST /v1/auth/challenge { "address": "addr1..." } → { "nonce": "...", "expires_at": "..." } # sign 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", ... } ``` 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 ``` 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) ``` ## Cache TTLs (Redis) ``` balance 60s tokens 60s transactions 30s block_latest 10s tx_details 300s (immutable) asset_info 120s pool_info 120s sync_status 5s protocol_params 300s utxos 10s ``` ## Validation Inputs hit regex gates before any DB query: - bech32 mainnet/testnet addresses (`addr1...` / `addr_test1...`) - 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). CIP-8 verification rejects non-EdDSA (`alg ≠ -8`), wrong key length, empty payload, payload-not-nonce, and bad key→address hash binding. ## Key storage 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.