Repository navigation
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Session duration, rate-limit security, capacity, and normal-use fan-out issues remain unresolved.
5 open findings
What changed in this PR
Adds abuse protection and wallet-ownership authentication to blockchain read APIs.
Changes:
- Adds per-IP rate limits and ownership-cookie guards.
- Implements multi-chain challenge signing and client-side session management.
- Adds extensive protocol, proxy, and cryptographic tests.
| File | Description |
|---|---|
vitest.config.ts |
Orders test aliases correctly. |
src/proxy.ts |
Adds rate limits and ownership guards. |
src/login/types.ts |
Extends BTC/XRP signer interfaces. |
src/login/adapters/privatekey.ts |
Implements BTC, DOGE, and XRP auth signing. |
src/hooks/useLedger/tatumBitcoinHandler.ts |
Authenticates Bitcoin history reads. |
src/chain/provider/XRP/xrpProvider.ts |
Authenticates XRP history reads. |
src/chain/provider/Solana/solanaProvider.ts |
Authenticates Solana history reads. |
src/chain/provider/EVM/evmProvider.ts |
Authenticates EVM history reads. |
src/chain/provider/Bitcoin/bitcoinProvider.ts |
Authenticates Bitcoin balance, UTXO, and raw transaction reads. |
src/app/api/chains/powSession.ts |
Implements signed challenges and sessions. |
src/app/api/chains/ownershipSignature.vitest.config.ts |
Configures isolated protocol tests. |
src/app/api/chains/ownershipSignature.ts |
Verifies ownership signatures by family. |
src/app/api/chains/ownershipSignature.test.ts |
Tests multi-chain signature verification. |
src/app/api/chains/ownershipSession.test.ts |
Tests challenge and session behavior. |
src/app/api/chains/ownershipRoutes.test.ts |
Tests routes, origins, and validation. |
src/app/api/chains/ownershipProtocol.ts |
Defines ownership scopes and messages. |
src/app/api/chains/OWNERSHIP.md |
Documents deployment and protocol behavior. |
src/app/api/chains/clientOwnershipSignature.ts |
Selects and invokes client signers. |
src/app/api/chains/clientAddressSession.ts |
Creates, caches, and refreshes sessions. |
src/app/api/chains/clientAddressSession.test.ts |
Tests client session orchestration. |
src/app/api/chains/chainsProxy.test.ts |
Tests proxy limits and guards. |
src/app/api/chains/auth/address/route.ts |
Exposes challenge and verification endpoints. |
biome.json |
Allows non-null assertions in tests. |
🧠 Review effort: Balanced
Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.
| import { normalizeOwnershipAddress, OwnershipScope } from "./ownershipProtocol" | ||
|
|
||
| const POW_CHALLENGE_TTL_MS = 5 * 60 * 1000 | ||
| const POW_SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000 |
Comment on lines
+60
to
+63
| const forwardedFor = request.headers.get("x-forwarded-for") | ||
| if (forwardedFor) { | ||
| return forwardedFor.split(",")[0]?.trim() ?? "unknown" | ||
| } |
| addressChallenge: { limit: 12, windowMs: 60 * 1000 }, | ||
| addressChallengeVerify: { limit: 24, windowMs: 60 * 1000 }, | ||
| balance: { limit: 30, windowMs: 60 * 1000 }, | ||
| history: { limit: 12, windowMs: 60 * 1000 }, |
Comment on lines
+82
to
+86
| if (!current || current.resetAt <= now) { | ||
| rateLimitStore.set(compositeKey, { | ||
| hits: 1, | ||
| resetAt: now + policy.windowMs, | ||
| }) |
Comment on lines
+197
to
+201
| if ( | ||
| request.method === "GET" && | ||
| isTxLookupPath(pathname) && | ||
| request.nextUrl.searchParams.get("verbose") !== "true" | ||
| ) { |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
Protects
/api/chains/*(the Tatum / RPC-backed blockchain read endpoints) from abuse by non-app clients.src/proxy.ts: per-IP, per-endpoint, 60s fixed window (in-memory, per instance). Over-limit returns429withRetry-After.Sec-Fetch-Site: cross-siteor anOriginthat doesn't match the host returns403.POST /api/chains/auth/address, new): the client requests a stateless HMAC-signed 5-minute challenge for(family, address), solves a SHA-256 proof of work (0000prefix) and signs the challenge with the wallet key. The server verifies both and sets an HttpOnly cookie valid only for that family+address on/api/chains(30 min, anchored to challenge issue time, so replays cannot extend it). No shared nonce storage is needed./api/chains/<family>/addresses/<addr>/(balance|history|utxos)require the matching cookie (401otherwise). Non-verboseGET /transactions/<hash>requires?address=plus the cookie. Verbose tx reads,feeandbroadcaststay unauthenticated (rate-limited, exceptbroadcast).signMessage), Solana (Ed25519), BTC/DOGE (auth-only SHA256 + compact ECDSA, not BIP322), XRP (ripple-keypairs). Added optionalsignAuthMessageto BTC/XRP signers.fetchWithAddressSession, which authenticates lazily, caches expiry, and retries once on401.src/app/api/chains.vitest.config.tsalias order fixed so@/configresolves. Biome allows!in*.test.ts.Limits per IP per 60s: auth challenge (GET) 12, auth verify (POST) 24, balance 30, history 12, utxos 18, tx lookup 30, fee 60.
Required deploy config (all environments)
API_POW_SESSION_SECRETAPI_OWNERSHIP_ORIGINhttps://wallet.example. HTTPS required in production. Defaults tohttp://localhost:3000in dev.The secret must be identical across instances (no shared state needed otherwise). Details in
src/app/api/chains/OWNERSHIP.md.Known limitations
feeand verbose tx reads are unauthenticated.QA guide
Setup: set the two env vars, run
yarn dev, open the app atAPI_OWNERSHIP_ORIGIN(e.g. http://localhost:3000). Use browser DevTools > Network (filterapi/chains) and Application > Cookies. Test with accounts for each chain family where available (EVM, Solana, BTC, DOGE, XRP). Use a fresh browser profile or clear cookies/sessionStorage between scenarios.A. Regression: normal wallet use (must not break)
GET /api/chains/auth/address?...(200) andPOST /api/chains/auth/address(200,Set-Cookie: gw_owner_v2_...; HttpOnly; Path=/api/chains), then reads return 200.gw-ownership-v2:<family>:<addr>), reads still 200.B. Ownership enforcement
Copy a real cookie from DevTools (name
gw_owner_v2_<hash>, value is the token).curl -i -H "Cookie: <name>=<value>" "$ORIGIN/api/chains/EVM/addresses/<own-address>/balance"returns 200.<address>in the URL. Expect401 {"message":"Address session required"}.EVMforBTC):401.401for/balance,/history,/utxos.401.GET /api/chains/BTC/transactions/<hash>withoutaddressgives400 "Wallet address is required for raw transaction reads". With?address=<own>and the cookie gives 200. With another address and the cookie gives401.GET /api/chains/BTC/transactions/<hash>?verbose=trueandGET /api/chains/BTC/feework without a cookie.401), the UI shows no error.POST /api/chains/auth/addresswith a made-up body returns400/401and noSet-Cookie. A POST with a wrongOriginheader returns403.C. Cross-origin protection
curl -i -H "Sec-Fetch-Site: cross-site" "$ORIGIN/api/chains/BTC/fee"returns403 {"message":"Forbidden"}.curl -i -H "Origin: https://evil.example" "$ORIGIN/api/chains/BTC/fee"returns403.Sec-Fetch-Site: same-originor no Origin header return 200.fetch("<ORIGIN>/api/chains/BTC/fee")is blocked or returns403.D. Rate limits
Use a loop with a fixed IP; local dev falls under one
unknown/same IP bucket, or send-H "x-real-ip: qa-1". Windows are 60s, so wait 60s between scenarios or change the IP value.Expected: first 60 return 200, then
429.GET /api/chains/auth/addressPOST /api/chains/auth/address/addresses/<a>/balance/addresses/<a>/history/addresses/<a>/utxos/transactions/<hash>/feeFor each, verify:
429with body{"message":"Too many requests"}and a numericRetry-Afterheader (at most 60).x-real-ipis unaffected.history, thenfeestill works).Retry-Afterseconds, requests succeed again.401responses become429after the limit).POST /api/chains/<family>/transactions/broadcastand non-chain routes (e.g./api/tokens) are never rate limited.E. Normal-usage sanity under limits
429s should appear in normal use (report any you see, with endpoint and count).429, confirm the UI degrades gracefully (loading/empty state, no crash) and recovers within a minute.F. Config failures
API_POW_SESSION_SECRET(or set under 32 chars) and restart:GET /api/chains/auth/address?family=EVM&address=0x...returns503, and address-scoped reads return401.API_OWNERSHIP_ORIGINwith a trailing slash (e.g.http://localhost:3000/): auth returns503.503).Automated checks
yarn test(all pass) andyarn biome check src/app/api/chains src/proxy.ts.