cardano-api: strip 'Fix #N:' audit-ticket prefixes from inline comments (was 50+ in main.py), drop hardening-pass changelog blocks from module docstring, rewrite README to drop deploy paths + marketing sections, keep tier/auth/TTL + policy IDs. cardano-checkout-py: drop TradeCraft lineage refs, swap chromaticcraft/tradecraft test fixtures for acme/globex, repository URL → git.sulkta.com.
148 lines
4.5 KiB
Markdown
148 lines
4.5 KiB
Markdown
# 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.
|