ChainSafe / ChainSafe/canton-middleware
Faucet API endpoints for devnet test tokens (DEMO, PROMPT, USDCX)
- Dominant language
- Go
- Stars
- 1
- Forks
- 1
- Avg merge
- 40m
- Merged PRs (30d)
- 1
Description
## Summary
Add faucet endpoints to the middleware so a public faucet web app can dispense the devnet test tokens (**DEMO**, **PROMPT**, **USDCX**) to any registered address — no wallet connection, rate-limited server-side.
**UI issue (approved design + frontend scope):** ChainSafe/canton-snap#96
The middleware is the natural home: it already has ledger access, the EVM→party mapping, and the drip logic in `scripts/utils/fund-wallet.go` — this feature essentially productizes `fund-wallet.go` behind rate-limited endpoints, executed from a dedicated funded faucet party with server-side signing (like the custodial transfer path).
All endpoints follow existing conventions: `snake_case` JSON, `{ "error": "..." }` error bodies, `{ "items": [...] }` list envelopes.
## Endpoints
### 1. `GET /api/v2/faucet/tokens`
Faucet config for the UI token selector (amounts/cooldowns not hardcoded client-side).
```json
{ "items": [
{ "symbol": "DEMO", "name": "Demo Token", "drip_amount": "100",
"cooldown_seconds": 3600, "kind": "direct", "enabled": true },
{ "symbol": "PROMPT", "name": "Prompt Token", "drip_amount": "50",
"cooldown_seconds": 3600, "kind": "direct", "enabled": true },
{ "symbol": "USDCX", "name": "USDC on Canton", "drip_amount": "250",
"cooldown_seconds": 3600, "kind": "offer", "enabled": true }
] }
```
`kind` reflects the existing offer-based distinction (`OFFER_BASED_TOKENS`) — the UI uses it to explain that USDCX arrives as a transfer offer to accept in the dapp. Drip amounts / cooldowns above are placeholders, final values TBD.
### 2. `POST /api/v2/faucet/drip`
The main action. Public / unauthenticated (no wallet connection by design), so all abuse control is server-side.
```json
// request
{ "address": "0x4f8AC1b02fA9DcB4f21e83Ec5700A3B2E1D09a21", "token": "DEMO", "captcha_token": "..." }
// 200 (direct token)
{ "tx_id": "1220ab...", "kind": "direct", "amount": "100",
"token": "DEMO", "next_available_at": "2026-07-10T12:00:00Z" }
// 200 (offer-based token)
{ "contract_id": "00ab...", "kind": "offer", "amount": "250", "token": "USDCX",
"expires_at": "2026-07-11T12:00:00Z", "next_available_at": "2026-07-10T12:00:00Z" }
```
Server-side flow:
1. Validate address + token; verify captcha if enabled.
2. Resolve the EVM address to its Canton party (same lookup as `GET /profile`) — recipients must already be registered.
3. Check cooldown (per address + token) and IP-level limits.
4. Execute the transfer from the faucet party (server-side signing). Direct for DEMO/PROMPT; for USDCX create a transfer offer with a `validity_seconds` (e.g. the existing 86400 default).
Error responses (distinct so the UI can render specific states):
| Status | Meaning | Body |
|---|---|---|
| `400` | Malformed address / unknown or disabled token / bad captcha | `{ "error": "..." }` |
| `404` | Address not registered as a Canton party | `{ "error": "address not registered" }` |
| `429` | Cooldown active | `{ "error": "cooldown active", "retry_after_seconds": 1740 }` |
| `503` | Faucet reserves depleted for that token | `{ "error": "faucet empty" }` |
### 3. `GET /api/v2/faucet/status?address=0x...`
Per-token readiness so the UI can show "ready now" vs. a countdown (and disable the button) before submitting.
```json
{ "items": [
{ "token": "DEMO", "available": true },
{ "token": "PROMPT", "available": true },
{ "token": "USDCX", "available": false, "retry_after_seconds": 1740 }
] }
```
### 4. `GET /api/v2/faucet/drips/recent?limit=10`
Powers the public "Recent drips" feed. Unauthenticated — truncate addresses server-side (`0x91bD…44eC`), same as the public transfer-list endpoints truncate party IDs.
```json
{ "items": [
{ "address": "0x91bD…44eC", "token": "USDCX", "amount": "250",
"kind": "offer", "created_at": "2026-07-10T11:58:03Z" }
] }
```
### 5. Health
Reuse the existing `GET /health` (nothing new needed if faucet routes live in the middleware).
## New infrastructure required
- **Faucet party**: a dedicated, funded Canton party whose key the middleware holds; reserves monitored/topped-up out-of-band.
- **Cooldown store**: per `(address, token)` last-drip timestamps (PostgreSQL is already in the stack).
- **Rate limiting**: net-new — nothing exists today (current abuse control is the manual registration whitelist). Needs per-address+token cooldown plus an IP-level limit; optional captcha (e.g. Cloudflare Turnstile) behind a config flag, hence `captcha_token`.
- **Config**: per-token drip amount / cooldown / enabled flag; devnet-only guard so these routes never ship enabled on mainnet.
## Out of scope
- Mainnet or any token with real value.
- Dispensing to raw `to_party_id` recipients (address-only keeps the public surface simpler/safer; can revisit).
- The faucet frontend itself — tracked in ChainSafe/canton-snap#96.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with scripts/utils/fund-wallet.go, the GET /profile address-to-party lookup, and the custodial transfer path to understand the existing drip and signing flows. Review the middleware's existing API conventions and GET /health, then determine how the PostgreSQL cooldown store, IP limits, token configuration, and devnet-only guard fit together. Done means the four documented faucet endpoints enforce the listed validation, cooldown, rate-limit, and response behavior for direct and offer-based tokens.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, postgresql
- Domain
- api, backend, databases, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100