cardano-api/README.md
Cobb Hayes aa8879bc69 Public-flip audit: drop audit-ticket prefixes + topology refs + AI scaffolding
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.
2026-05-27 11:15:02 -07:00

4.5 KiB

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.