feat(solana-rpc): method-aware tier failover (self-hosted → indexed → public) - #18
Open
sanjeevkkansal wants to merge 2 commits into
Open
feat(solana-rpc): method-aware tier failover (self-hosted → indexed → public)#18sanjeevkkansal wants to merge 2 commits into
sanjeevkkansal wants to merge 2 commits into
Conversation
…-> 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
approved these changes
Jul 25, 2026
anil-rome
left a comment
Contributor
There was a problem hiding this comment.
Approving. Verified against the diff, not just the description:
- Secret handling is clean —
SOLANA_RPC_INDEXED_URL/SOLANA_RPCstay server-side (noNEXT_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 failovercatchand 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:
SCAN_METHODSis defined independently in each frontend — drift risk if a new scan method is added to only one; worth centralizing eventually.getSignaturesForAddressisn'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.)
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.
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:
SOLANA_RPC(self-hosted) →SOLANA_RPC_INDEXED_URL→ publicSOLANA_RPC_INDEXED_URL→ public — never self-hostedScans 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 (
sendTransactiondouble-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). ExistingresolveSolanaRpcUpstreamprecedence 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.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.