Skip to content

feat(faucet): testnet faucet via taps wallet daemon#9

Open
craftsoldier wants to merge 35 commits into
Kenbak:mainfrom
zcashme:feat/testnet-faucet
Open

feat(faucet): testnet faucet via taps wallet daemon#9
craftsoldier wants to merge 35 commits into
Kenbak:mainfrom
zcashme:feat/testnet-faucet

Conversation

@craftsoldier
Copy link
Copy Markdown
Contributor

@craftsoldier craftsoldier commented May 24, 2026

finally got the testnet faucet wired up. lives at /faucet on testnet, with a "Get TAZ" CTA in the navbar (testnet-only) pointing at it so people can actually find it.

drag a slider (anywhere from 0.001 to 1 TAZ), drop in a utest1… orchard unified address, hit send — a few seconds later there's a confirmed tx and a "view tx →" link straight back into the explorer. wallet stats card up top shows live balance + how much is actually spendable right now, and there's a donate QR + address at the bottom for refills (pulled live from the wallet, so it'll always be the right one).

the actual sends are handled by taps — a small rust wallet daemon i wrote for exactly this. cipherscan-express never holds a key; it just sits in front of taps and does the captcha. taps takes the request, builds a shielded orchard tx, signs it, broadcasts via lightwalletd. clean separation — keys can rotate, wallet implementations can swap, none of it touches cipherscan.

defenses on the dispense path are turnstile + the 1 TAZ per-tx cap + the global express rate limiter. no per-address cooldown / no redis dependency — kept it simple, can always add a daily-volume cap later if abuse becomes a thing.

new dep

one runtime dep added: @marsidev/react-turnstile (^1.5.2) — thin react wrapper around cloudflare's turnstile widget script. only thing pulled in for this PR.

wiring it up

taps already runs on our infra at https://light.zcash.me/taps — no docker / wallet setup on your end. two env vars on the express side:

TAPS_URL=https://light.zcash.me/taps
TAPS_API_KEY=<i'll send this over DM>

i'll DM the api key separately so it doesn't end up in a screenshot. the wallet's already funded and synced; donations from the /faucet donate card go straight to it.

captcha

cloudflare turnstile is wired up but stays off until both keys are set. two pieces, both from the same widget config:

  1. go to https://dash.cloudflare.com/?to=/:account/turnstile, create a widget, whitelist testnet.cipherscan.app
  2. site key → netlify as NEXT_PUBLIC_TURNSTILE_SITE_KEY
  3. secret key → vps express as TURNSTILE_SECRET_KEY

both or neither. the UI now drives its captcha gate off /api/faucet/status (which mirrors the server's secret-key presence), so if the server enforces but the site key is missing in the build, you get a loud "captcha misconfigured" banner instead of a silent 400 loop.

gotcha

if our taps daemon is briefly out of sync with the chain tip (after a restart, say), dispenses 503 with wallet still syncing and the UI shows a "wallet syncing" notice up top (kicks in when a single dispense can fulfill less than 20% of the per-tx cap). usually clears in under a minute. on our end the container has restart: unless-stopped so it survives reboots.

oh — /swap and /faucet both have network-gating now (/swap 404s outside mainnet, /faucet 404s outside testnet) so neither shows up on the wrong network.

test plan

  • express env set, `curl https://api.testnet.cipherscan.app/api/faucet/status\` returns balance + UA + `maxDispensableTaz`
  • testnet.cipherscan.app/faucet — slider works, dispense returns a txid that resolves in the explorer
  • navbar shows "Get TAZ" on testnet, hidden on mainnet
  • if turnstile keys set: widget renders, invalid token → `captcha failed`; missing site key while server enforces → "captcha misconfigured" banner

@netlify
Copy link
Copy Markdown

netlify Bot commented May 24, 2026

👷 Deploy request for cipherscan pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 4644942

@netlify
Copy link
Copy Markdown

netlify Bot commented May 24, 2026

👷 Deploy request for cipherscan-crosslink pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 4644942

@craftsoldier craftsoldier marked this pull request as draft May 24, 2026 04:29
@Kenbak
Copy link
Copy Markdown
Owner

Kenbak commented May 24, 2026

couple things before we merge:

need to fix:

add a per-IP rate limit on /api/faucet/dispense, right now the only gate is turnstile + the 1 TAZ cap + our global express limiter. turnstile is bypassable with effort and the global limiter isn't faucet-specific. a simple in-memory map (5 req/IP/hour, no redis needed) would stop someone draining the wallet in a few minutes. it's testnet so not critical but good practice

enforce max amount server-side, the slider caps at maxSpendTaz on the client but the server forwards whatever amountTaz comes in. taps probably validates too but we should reject amountTaz > 1 (or whatever the cap is) before proxying. defense in depth

should check:

balanceTaz unit, you divide max_dispensable_zat by 1e8 to get TAZ, but balanceTaz uses taps.balances.orchard raw. if taps returns that in zatoshis the wallet stats card will show 450M instead of 4.5. just verify which unit taps sends

confirm trust proxy is set correctly on express so req.ip in the turnstile verify is the real client IP not the LB

overall this is good to merge with the rate limit + server-side max.

@craftsoldier
Copy link
Copy Markdown
Contributor Author

addressed all four, pushed in 7afd3d2 and 4644942:

  • rate limit: added a per-IP limiter on /api/faucet/dispense via express-rate-limit — 5 successful dispenses per IP per hour, in-memory, skipFailedRequests: true so captcha typos don't burn budget. global limiter + turnstile stay on top.
  • server-side max: went with "whatever the cap is." after captcha, /dispense hits taps /status and rejects amountTaz > max_dispensable_zat. that's the live ceiling taps already publishes (min(max_spend_zat, spendable - fee), rounded to increment), so cipherscan + taps + slider all read the same field — nothing drifts.
  • balanceTaz unit: verified — taps divides in routes.rs:229 (orchard: (...into_u64() as f64) / 1e8), so balances.orchard arrives as TAZ. raw passthrough is correct.
  • trust proxy: confirmed at server/api/server.js:257.

also slipped a memo into every dispense plugging zipher (in beta) — testnet recipients should know:

thanks for using the cipherscan testnet faucet — zipher, a zcash wallet for humans and agents, coming soon (in beta)

@craftsoldier craftsoldier marked this pull request as ready for review May 24, 2026 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants