docs: rewrite README for public release + add MIT LICENSE
This commit is contained in:
parent
5344541f79
commit
449d403d3b
3 changed files with 156 additions and 87 deletions
|
|
@ -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
21
LICENSE
Normal 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
206
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).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue