Skip to content

feat(solana-rpc): method-aware tier failover (self-hosted → indexed → public) - #18

Open
sanjeevkkansal wants to merge 2 commits into
mainfrom
fix-aerarium-rpc-tiers
Open

feat(solana-rpc): method-aware tier failover (self-hosted → indexed → public)#18
sanjeevkkansal wants to merge 2 commits into
mainfrom
fix-aerarium-rpc-tiers

Conversation

@sanjeevkkansal

Copy link
Copy Markdown

Why

The Solana lane currently rides ONE upstream. Two problems: (a) the flows page's stranded-asset check is getTokenAccountsByOwner — on the unindexed internal devnet RPC that's a full token-program sweep (the 2026-07-24 node2 wedge class, https://github.com/rome-protocol/rome-ops/pull/875); (b) pointing the whole lane at a paid provider instead (as https://github.com/rome-protocol/rome-ops/pull/881 first did) makes a provider outage take down Phantom-lane submission entirely.

Method-aware failover fixes both:

traffic chain
point reads / DoTxUnsigned submit SOLANA_RPC (self-hosted) → SOLANA_RPC_INDEXED_URL → public
scan-class (gTABO et al.) SOLANA_RPC_INDEXED_URL → public — never self-hosted

Scans never touch the self-hosted tier because an unindexed owner-scan keeps sweeping server-side even after the client disconnects — a "try internal, then fall back" chain would still land every sweep on the box. Failover triggers: connect error / timeout / 429 / 5xx only; a JSON-RPC application error (200 + error body) is an answer and is mirrored, not retried (sendTransaction double-submit through failover is safe — same signature, duplicates rejected on-chain).

Changes

  • lib/solanaRpc.ts: isScanPayload (wire-level scan methods; batch = scan if any entry scans) + resolveSolanaRpcTiers (dedup, drops unset tiers). Existing resolveSolanaRpcUpstream precedence untouched — it is the self-hosted tier.
  • app/api/solana-rpc/route.ts: walks the tier chain; body still forwarded verbatim (parse is classification-only; unparseable bodies degrade to the point-read chain); per-class timeouts (8s point-read / 30s scan); generic 502 when all tiers fail, no upstream hostnames leaked.
  • New env SOLANA_RPC_INDEXED_URL (optional — unset keeps today's behavior minus the 5xx-mirror, which now fails over). Ops wiring in rome-ops #881.

Tests

24 new/updated across lib/__tests__/solanaRpc.test.ts + app/api/solana-rpc/__tests__/route.test.ts: tier selection per class, scan-never-self-hosted (with and without indexed set), failover on connect error/429/5xx, app-error passthrough, verbatim forwarding, no-leak exhaustion. Full suite 753 passed; production build clean.

…-> public)

Point reads / DoTxUnsigned submission try SOLANA_RPC first, then the new
SOLANA_RPC_INDEXED_URL, then public. Scan-class methods (the flows page's
getTokenAccountsByOwner stranded-asset check) skip the self-hosted tier
entirely - an unindexed node serves them as full token-program sweeps
that keep running after client disconnect (2026-07-24 devnet RPC wedge).
Failover on connect error / timeout / 429 / 5xx; JSON-RPC application
errors are answers, not failures. Body still forwarded verbatim - parse
is classification-only, unparseable bodies degrade to point-read chain.
…tier

A devnet public fallback on a mainnet deploy would silently answer from
the wrong cluster - worse than failing. Mainnet deploys set the override
to api.mainnet-beta.solana.com; devnet/testnet keep the default.

@anil-rome anil-rome left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. Verified against the diff, not just the description:

  • Secret handling is cleanSOLANA_RPC_INDEXED_URL / SOLANA_RPC stay server-side (no NEXT_PUBLIC_, not exposed via /api/env); the client hits the same-origin route, so the keyed URL never reaches the bundle or network tab. The failover catch and exhaustion path don't log the message (would carry the keyed host).
  • Failover semantics are right — advances only on connect error / timeout / 429 / 5xx; a JSON-RPC application error (200 + error body) and 4xx are mirrored, not retried, so a DoTxUnsigned submit isn't re-routed across tiers (and a duplicate submit would be signature-rejected on-chain anyway).
  • Scan-class → indexed → public (never self-hosted); point/submit → self-hosted → indexed → public; tiers deduped + unset-dropped, so behavior is unchanged until the indexed var is set.
  • Body forwarded verbatim (parse is classification-only; unparseable → point-read chain); generic 502 on exhaustion with no upstream host leaked.
  • Tests cover scan-never-self-hosted (with and without indexed set), failover triggers, app-error passthrough, verbatim forwarding, and no-leak exhaustion.

Two non-blocking nits:

  1. SCAN_METHODS is defined independently in each frontend — drift risk if a new scan method is added to only one; worth centralizing eventually.
  2. getSignaturesForAddress isn't in the scan set — intentional? It's not a full-accounts sweep, but it's address-index-dependent on an unindexed node, so worth a conscious decision. (We're auditing the full RomeEVM RPC access pattern separately for this class of gap.)

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