docs: tidy inline comments and rewrite README

This commit is contained in:
Sulkta 2026-05-27 11:15:02 -07:00
parent 5b3b9010e0
commit 6b994a6944
2 changed files with 230 additions and 284 deletions

246
README.md
View file

@ -1,142 +1,148 @@
# Cardano Chain Data API
# cardano-api
REST API for querying Cardano blockchain data via db-sync PostgreSQL database.
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** — async Python API framework
- **asyncpg** — async PostgreSQL driver
- **Redis** — rate limiting + response caching
- 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
## Deployment
## Run
```bash
cd /opt/cardano/dbsync
sudo docker compose up -d --build
```
docker compose up -d --build
```
API runs on `127.0.0.1:8765` (localhost only, VPN access via future rebind to `192.0.2.10:8765`).
Listens on `:8765` inside the container. Wire it to whatever proxy / port-forward the deploy wants.
## Authentication
## Tiers + rate limits
### Rate Limits
- Anonymous (no key): 20 req/min per IP
- Standard API key: 100 req/min
- Elevated API key: 1000 req/min
- Master key: unlimited
| 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 |
### Using API Keys
```bash
# Header (preferred)
curl -H "X-API-Key: capi_xxx" http://127.0.0.1:8765/v1/block/latest
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)
# Query param
curl http://127.0.0.1:8765/v1/block/latest?api_key=capi_xxx
```
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
### Sync Status
```
GET /v1/sync/status
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)
```
### Blocks
```
GET /v1/block/latest
GET /v1/block/{block_no}
```
### Addresses
```
GET /v1/address/{address}/balance
GET /v1/address/{address}/tokens
GET /v1/address/{address}/transactions?page=1&limit=20&order=desc
```
### Transactions
```
GET /v1/tx/{tx_hash}
```
### Assets
```
GET /v1/asset/{policy_id}/info
GET /v1/asset/{policy_id}/{asset_name}/holders?limit=20
```
### Pools
```
GET /v1/pool/{pool_id}/info
```
### Admin (master key required)
```
POST /admin/keys — create API key
DELETE /admin/keys/{key} — revoke key
GET /admin/keys — list all keys
GET /admin/stats — usage stats
```
## Known Policy IDs
- **TRP**: `9c4bd4a90cdb73d9ff681215ecf7dea9fb183d916d30487d17098e05`
- **MAP**: `24bd9e7b9ae3a61df79eca72fd8355d0f7767e4c55a04a0d919c019c`
## Future: TRP Token Gating
Design for decentralized, permissionless API access based on TRP token holdings:
### Tier Mapping
- 0 TRP → anonymous rate limits (20 req/min)
- 50+ TRP → standard tier (100 req/min)
- 500+ TRP → elevated tier (1000 req/min)
### Implementation Plan
1. `POST /admin/keys/verify-trp` endpoint
2. Takes Cardano address, queries `/v1/address/{addr}/tokens`
3. Checks TRP policy balance
4. Auto-creates or upgrades API key based on holdings
5. Stores `owner` (address) and `trp_balance` in key hash
### Data Model (already in place)
API keys stored in Redis as:
```
apikey:<key> → {
tier: "standard"|"elevated",
label: "...",
owner: "addr1...", # Cardano address
trp_balance: 500, # Last verified TRP balance
created_at: "..."
}
```
### Benefits
- **Permissionless**: Anyone can verify holdings and get access
- **Decentralized**: No manual approval needed
- **Incentivized**: Holding TRP = better API access
- **Revocable**: Re-verify periodically to maintain tier
## Caching
Response TTLs:
- Balance/tokens: 60s
- Transactions: 30s
- Latest block: 10s
- TX details: 300s (immutable)
- Asset info: 120s
- Pool info: 120s
- Sync status: 5s
## Environment Variables
## Cache TTLs (Redis)
```
DB_HOST=postgres-dbsync
DB_PORT=5432
DB_NAME=cexplorer
DB_USER=dbsync
DB_PASS=...
REDIS_HOST=redis-api
REDIS_PORT=6379
API_MASTER_KEY=capi_...
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.