From 449d403d3b766361dca3b2b45225a2f606584a65 Mon Sep 17 00:00:00 2001 From: Sulkta Date: Sun, 28 Jun 2026 12:51:08 -0700 Subject: [PATCH] docs: rewrite README for public release + add MIT LICENSE --- .forgejo/workflows/gitleaks.yml | 16 +-- LICENSE | 21 ++++ README.md | 206 ++++++++++++++++++++------------ 3 files changed, 156 insertions(+), 87 deletions(-) create mode 100644 LICENSE diff --git a/.forgejo/workflows/gitleaks.yml b/.forgejo/workflows/gitleaks.yml index 77d88db..d77cda0 100644 --- a/.forgejo/workflows/gitleaks.yml +++ b/.forgejo/workflows/gitleaks.yml @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4430ab4 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index 4b53aed..fd5ca20 100644 --- a/README.md +++ b/README.md @@ -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).