docs: rewrite README for public release + add MIT LICENSE

This commit is contained in:
Sulkta 2026-06-28 12:51:08 -07:00
parent 5344541f79
commit 449d403d3b
3 changed files with 156 additions and 87 deletions

View file

@ -1,16 +1,8 @@
# .forgejo/workflows/gitleaks.yml
# Gitleaks secret-scanning workflow.
#
# Gitleaks CI workflow for secret scanning. Use it as a CI secret-scan step, e.g. at
# `.forgejo/workflows/gitleaks.yml` after the Forgejo act_runner is registered
# (after a CI runner is configured).
#
# Pairs with the server-side pre-receive hook — that one is
# the strict enforcement layer (rejects the push); this one provides the
# per-PR red ✗ that branch-protection rules can require before merge.
#
# Layer 1 (this workflow): visible per-PR status, can be a required check.
# Layer 2 (pre-receive hook): strict enforcement at the server.
# Layer 3 (scheduled cron sweep): nightly full-history sweep across all repos.
# Scans the repository for committed secrets on every push and pull request,
# so credentials never land in history unnoticed. It runs on a Forgejo/Gitea
# Actions runner; copy it to .github/workflows/ to run it on GitHub Actions too.
name: gitleaks

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sulkta
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

206
README.md
View file

@ -1,57 +1,138 @@
# cardano-api
REST API over cardano-db-sync + cardano-node. FastAPI + asyncpg + Redis.
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 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.
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).
## Stack
## Features
- 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
- 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
## Run
## 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
```
Listens on `:8765` inside the container. Wire it to whatever proxy / port-forward the deploy wants.
The API listens on port `8765`. Put it behind your own reverse proxy / TLS
terminator as needed.
## Tiers + rate limits
To run without Docker:
| Tier | TRP needed | Rate (req/min) | tx submit (per min) | Node read | Node submit |
```
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 |
| 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.
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`.
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.
### Token-gated keys (optional, self-service)
## Auth flow (TRP-gated)
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": "..." }
-> { "nonce": "...", "expires_at": "..." }
# sign nonce with the wallet via CIP-8
# sign the nonce with the wallet via CIP-8
POST /v1/auth/verify { "address", "nonce", "signature", "key" }
→ { "api_key": "capi_...", "tier", "trp_balance" }
-> { "api_key": "capi_...", "tier", "trp_balance" }
POST /v1/auth/refresh (X-API-Key header — self-service only)
→ { "tier", "trp_balance", "expires_at", ... }
-> { "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
```
@ -87,62 +168,37 @@ POST /admin/refresh-tiers (master)
GET /admin/stats (master)
```
## Cache TTLs (Redis)
## Caching
Responses are cached in Redis with per-endpoint TTLs (seconds):
```
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
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
```
## Validation
## Security notes
Inputs hit regex gates before any DB query:
- 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.
- bech32 mainnet/testnet addresses (`addr1...` / `addr_test1...`)
- 64-hex tx hashes
- 56-hex policy IDs
## Contributing
Tx submit body is capped at 64 KB (middleware reads the actual stream; chunked transfer can't bypass).
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.
CIP-8 verification rejects non-EdDSA (`alg ≠ -8`), wrong key length, empty payload, payload-not-nonce, and bad key→address hash binding.
## License
## 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.
Released under the MIT License. See [LICENSE](LICENSE).