Skip to content

Replace Chainstack with public RPC provider pools#40

Merged
feruzm merged 4 commits into
mainfrom
feature/public-chain-providers
Jul 6, 2026
Merged

Replace Chainstack with public RPC provider pools#40
feruzm merged 4 commits into
mainfrom
feature/public-chain-providers

Conversation

@feruzm

@feruzm feruzm commented Jul 6, 2026

Copy link
Copy Markdown
Member

Moves the external-chain balance and RPC-proxy endpoints off the Chainstack vendor onto static, ordered pools of public endpoints, each with per-provider failover. No client changes are required: /private-api/balance/:chain/:address keeps the same response shape (echoed chain, integer base-unit balance string, unit), and /private-api/rpc/:chain stays a transparent JSON-RPC proxy.

Providers (all keyless by default; optional free keys only)

  • BTC — Esplora dialect (mempool.space, then Blockstream), integer satoshi. This also fixes the old default path returning a decimal BTC string.
  • ETH / BNB / SOL — public JSON-RPC pools (PublicNode plus independent operators / official dataseeds).
  • Pools are overridable per environment via ETH_RPC_URLS, BNB_RPC_URLS, SOL_RPC_URLS, BTC_ESPLORA_URLS (comma-separated), so endpoints can be swapped without a code change.

Scope reductions

  • Removed POST /private-api/broadcast/:chain. Transactions are signed and broadcast client-side through MetaMask; the endpoint had no consumers.
  • Removed TRON / TON / APT balance handlers and config — none are exposed in the wallet UI (BTC/ETH/BNB/SOL only).

Hardening

  • Per-chain address validation, which also rejects path-traversal attempts through the balance route and returns 400 on malformed input instead of walking the whole provider pool.
  • 10s per-provider timeout, a short-lived balance cache (shared node-cache) and in-flight request dedup.
  • Native BigInt in place of the hand-rolled hex bignum for EVM balances.

Config / env

  • Drops CHAINSTACK_API_KEY and the defunct Chainz BTC fallback (CHAINZ_API_KEY).
  • BLOCKSTREAM_CLIENT_ID/SECRET remain optional (Blockstream enterprise BTC fallback); adds optional HELIUS_API_KEY as an extra Solana fallback.

Balance and RPC paths were exercised live against the new pools (correct integer balances per chain, failover on a dead primary, validation → 400, RPC proxy getLatestBlockhash).

Summary by CodeRabbit

  • New Features

    • Added configurable RPC and Esplora endpoint lists for supported chains, including optional Blockstream and Helius authentication.
    • Improved balance and blockchain request handling with automatic provider failover and cached responses.
  • Bug Fixes

    • Strengthened validation for chain and address inputs.
    • Made balance results more consistent and improved handling of upstream errors and rate limits.
  • Chores

    • Updated setup documentation and deployment configuration to reflect the new environment variables.
    • Removed support for several previously listed chain configurations.

Move external-chain balance and RPC-proxy endpoints off the Chainstack
vendor onto static, ordered pools of public endpoints with per-provider
failover:

- BTC balances via Esplora (mempool.space, Blockstream), integer sats
- ETH/BNB/SOL via public JSON-RPC pools (PublicNode + independents)
- Optional env overrides: ETH_RPC_URLS / BNB_RPC_URLS / SOL_RPC_URLS / BTC_ESPLORA_URLS

Scope reductions:
- Drop the /private-api/broadcast/:chain endpoint: transactions are signed
  and broadcast client-side via MetaMask, so it had no consumers
- Drop TRON/TON/APT handlers, which are not exposed in the UI

Hardening:
- Per-chain address validation (also blocks path traversal via the balance
  route), returning 400 on bad input instead of walking the provider pool
- 10s per-provider timeout, short-lived balance cache with in-flight dedup
- Replace hand-rolled hex bignum with native BigInt

Removes CHAINSTACK_API_KEY and the defunct Chainz BTC fallback.
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@feruzm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99461b8b-77c2-42b0-bcb5-032e1c08927e

📥 Commits

Reviewing files that changed from the base of the PR and between 77e21cb and a9c517c.

📒 Files selected for processing (3)
  • README.md
  • src/server/chain-providers.ts
  • src/server/handlers/private-api.ts
📝 Walkthrough

Walkthrough

This PR replaces Chainstack-specific balance/broadcast/RPC logic with a chain-provider pool architecture. New chain-providers.ts defines ETH/BNB/SOL/BTC provider pools with Helius and Blockstream auth support. private-api.ts and helper.ts are rewritten to fetch balances via failover across these pools with caching. Deprecated chain configs, the broadcast route, and Chainstack env vars/dependencies are removed.

Changes

Provider pool based chain balance/RPC refactor

Layer / File(s) Summary
Chain provider pool definitions
src/server/chain-providers.ts
New module exports RpcProvider/EsploraProvider interfaces and ETH_RPC_POOL, BNB_RPC_POOL, SOL_RPC_POOL, BTC_ESPLORA_POOL, with env-based overrides and Helius/Blockstream auth support.
Esplora balance fetching helper
src/server/helper.ts
ChainBalanceResponse becomes provider-agnostic; fetchBlockstreamBalance is replaced by exported fetchEsploraBalance with bearer-auth retry on 401 and rate-limit header extraction; getBlockstreamAccessToken is unexported.
Private API balance and JSON-RPC proxy rewrite
src/server/handlers/private-api.ts
Introduces tryProviders, jsonRpcPost, TerminalProviderError, caching/in-flight dedup, CHAIN_HANDLERS, and rewrites fetchChainBalance, balance, and chainRpc to use provider-pool failover with method allowlists.
Deprecated route/config removal and asset icons
src/server/index.tsx, src/server/handlers/wallet-api.ts, src/server/handlers/constants.ts
Removes the broadcast route, removes tron/ton/apt from CHAIN_CONFIG, and updates ASSET_ICON_URLS entries.
Environment variable and dependency updates
README.md, docker-compose.yml, package.json
Removes CHAINSTACK_API_KEY/CHAINZ_API_KEY/bs58check, adds Blockstream/Helius/RPC override env vars.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant balance
  participant fetchChainBalance
  participant tryProviders
  participant RpcProvider

  Client->>balance: GET balance(chain, address)
  balance->>fetchChainBalance: fetchChainBalance(chain, address)
  fetchChainBalance->>tryProviders: fetchBalance via provider pool
  tryProviders->>RpcProvider: jsonRpcPost / fetchEsploraBalance
  RpcProvider-->>tryProviders: result or error
  tryProviders-->>fetchChainBalance: balance or TerminalProviderError
  fetchChainBalance-->>balance: cached/fresh result
  balance-->>Client: response with x-provider header
Loading

Possibly related PRs

  • ecency/vision-api#15: Both PRs heavily refactor src/server/handlers/private-api.ts around fetchChainBalance and the balance endpoint's error/header handling.

Poem

A rabbit hops from node to node,
Chainstack's gone, new pools bestowed 🐇
Helius keys and Blockstream tokens bright,
Failover hops through the night,
No more broadcast, tron, or ton to keep —
Just clean pools where balances sleep. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: replacing Chainstack-backed handling with public RPC provider pools.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/public-chain-providers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 602bd18e3e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

headers: JSON_RPC_HEADERS,
timeout: PROVIDER_TIMEOUT_MS,
});
return { providerId: provider.id, data: response.data };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Throw on JSON-RPC errors before accepting a provider

When the first public RPC endpoint responds with HTTP 200 but a JSON-RPC error object (for example, a provider-side rate limit or an allowed method disabled on that endpoint), this callback returns it as a successful tryProviders result, so the remaining healthy providers are never attempted and the proxy surfaces the first provider's failure. The balance path already treats data.error as a provider failure; this proxy path should do the same for provider-side JSON-RPC errors or otherwise classify client parameter errors explicitly.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the Chainstack vendor dependency with static, ordered pools of keyless public RPC and Esplora endpoints (with optional env-var overrides), removing the broadcast endpoint and TRON/TON/APT balance handlers that had no active callers.

  • Balance endpoint hardening: per-address validation (path-traversal safe), 10 s per-provider timeout, 15 s balance cache, in-flight dedup, and a custom raw-text lamport extractor to preserve Solana u64 precision; BTC balance now correctly returns integer satoshis via BigInt arithmetic.
  • New transparent RPC proxy (POST /private-api/rpc/:chain) with a method allowlist; correctly fails over on both HTTP and JSON-RPC application-level errors.
  • Config simplification: drops CHAINSTACK_API_KEY and CHAINZ_API_KEY; adds optional HELIUS_API_KEY and per-chain *_RPC_URLS overrides documented in README and docker-compose.

Confidence Score: 5/5

Safe to merge; the core balance and proxy paths are well-structured and the previous Solana precision gap in the balance endpoint has been correctly addressed.

The rewrite is straightforward: static pools replace a vendor dependency with no API contract changes for callers. Address validation, failover, caching, and BTC bignum handling are all correctly implemented. The only new concern — that the RPC proxy's transparent getBalance path for Solana uses standard JSON parsing rather than the raw-text extraction used by the balance endpoint — is theoretical given that the total SOL supply is well below the precision limit, and the balance endpoint itself is correctly fixed.

src/server/handlers/private-api.ts — specifically the ALLOWED_RPC_METHODS.sol entry for getBalance in chainRpc, which does not apply the same u64 raw-text extraction used by the dedicated balance endpoint.

Important Files Changed

Filename Overview
src/server/chain-providers.ts New file defining static RPC/Esplora provider pools per chain with env-var overrides; Helius key correctly placed in URL query param per Helius requirements; pool construction and bearerAuth inference logic are clean.
src/server/handlers/private-api.ts Replaces Chainstack-backed balance and RPC handlers with pool-based failover; balance endpoint is well-hardened (address validation, u64-safe lamport extraction, in-flight dedup, short cache); chainRpc proxy handles JSON-RPC application errors and failover correctly, but the Solana getBalance method in the proxy allowlist uses standard JSON parsing that does not apply the u64 raw-text extraction used by the dedicated balance endpoint.
src/server/helper.ts fetchEsploraBalance correctly computes BTC satoshi balance via BigInt arithmetic including unconfirmed mempool amounts; Blockstream OAuth token caching and 401 retry logic are correct.
src/server/handlers/constants.ts Removes icon URLs for APT/TON/TRON chains that no longer have handlers; straightforward cleanup.
src/server/handlers/wallet-api.ts Only change is importing fetchChainBalance from the new private-api module; no logic changes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant BalanceRoute as GET /balance/:chain/:address
    participant RpcRoute as POST /rpc/:chain
    participant tryProviders
    participant Pool as Provider Pool<br/>(ETH/BNB/SOL/BTC)
    participant Cache

    Client->>BalanceRoute: request balance
    BalanceRoute->>BalanceRoute: validateAddress (regex)
    BalanceRoute->>Cache: check cache
    alt cache hit
        Cache-->>BalanceRoute: cached response
        BalanceRoute-->>Client: 200 JSON
    else cache miss
        BalanceRoute->>tryProviders: fetchBalance(address)
        loop each provider until success
            tryProviders->>Pool: HTTP request (timeout 10s)
            alt provider error
                Pool-->>tryProviders: error/timeout
                tryProviders->>tryProviders: warn + try next
            else success
                Pool-->>tryProviders: balance data
                tryProviders-->>BalanceRoute: ChainBalanceResponse
            end
        end
        BalanceRoute->>Cache: set(key, response, 15s)
        BalanceRoute-->>Client: "200 JSON {chain, balance, unit}"
    end

    Client->>RpcRoute: "JSON-RPC body {method, params}"
    RpcRoute->>RpcRoute: validate chain + method allowlist
    RpcRoute->>tryProviders: proxy RPC call
    loop each provider until success
        tryProviders->>Pool: axios.post(body, timeout 10s)
        alt JSON-RPC error body (HTTP 200)
            Pool-->>tryProviders: "{error: ...}"
            tryProviders->>tryProviders: throw rpcErr (failover)
        else HTTP error
            Pool-->>tryProviders: non-2xx
            tryProviders->>tryProviders: failover
        else success
            Pool-->>tryProviders: JSON-RPC result
            tryProviders-->>RpcRoute: "{providerId, data}"
        end
    end
    RpcRoute-->>Client: 200 JSON (raw provider response)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Client
    participant BalanceRoute as GET /balance/:chain/:address
    participant RpcRoute as POST /rpc/:chain
    participant tryProviders
    participant Pool as Provider Pool<br/>(ETH/BNB/SOL/BTC)
    participant Cache

    Client->>BalanceRoute: request balance
    BalanceRoute->>BalanceRoute: validateAddress (regex)
    BalanceRoute->>Cache: check cache
    alt cache hit
        Cache-->>BalanceRoute: cached response
        BalanceRoute-->>Client: 200 JSON
    else cache miss
        BalanceRoute->>tryProviders: fetchBalance(address)
        loop each provider until success
            tryProviders->>Pool: HTTP request (timeout 10s)
            alt provider error
                Pool-->>tryProviders: error/timeout
                tryProviders->>tryProviders: warn + try next
            else success
                Pool-->>tryProviders: balance data
                tryProviders-->>BalanceRoute: ChainBalanceResponse
            end
        end
        BalanceRoute->>Cache: set(key, response, 15s)
        BalanceRoute-->>Client: "200 JSON {chain, balance, unit}"
    end

    Client->>RpcRoute: "JSON-RPC body {method, params}"
    RpcRoute->>RpcRoute: validate chain + method allowlist
    RpcRoute->>tryProviders: proxy RPC call
    loop each provider until success
        tryProviders->>Pool: axios.post(body, timeout 10s)
        alt JSON-RPC error body (HTTP 200)
            Pool-->>tryProviders: "{error: ...}"
            tryProviders->>tryProviders: throw rpcErr (failover)
        else HTTP error
            Pool-->>tryProviders: non-2xx
            tryProviders->>tryProviders: failover
        else success
            Pool-->>tryProviders: JSON-RPC result
            tryProviders-->>RpcRoute: "{providerId, data}"
        end
    end
    RpcRoute-->>Client: 200 JSON (raw provider response)
Loading

Reviews (4): Last reviewed commit: "Harden provider failover edge cases" | Re-trigger Greptile

Comment thread src/server/handlers/private-api.ts Outdated
Comment thread src/server/chain-providers.ts Outdated
- RPC proxy: fail over to the next provider when one returns a JSON-RPC
  error body over HTTP 200 (e.g. a rate-limited node), while still passing
  the error envelope through if every provider returns one.
- Solana balance: read the u64 lamport value from the raw response text so
  balances above 2^53 aren't truncated by JSON double parsing.
- Helius: pass the API key as an Authorization bearer header instead of a
  URL query param, keeping it out of access logs and error traces.
Comment thread src/server/chain-providers.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
README.md (1)

57-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Docker env docs missing RPC override vars.

The dev "Environment variables" section documents ETH_RPC_URLS, BNB_RPC_URLS, SOL_RPC_URLS, and BTC_ESPLORA_URLS, but the Docker section only adds HELIUS_API_KEY. docker-compose.yml passes through all four RPC override vars to the container, so Docker users won't know these overrides are supported.

📝 Proposed fix
  * `BLOCKSTREAM_CLIENT_ID`
  * `BLOCKSTREAM_CLIENT_SECRET`
  * `HELIUS_API_KEY`
+ * `ETH_RPC_URLS`
+ * `BNB_RPC_URLS`
+ * `SOL_RPC_URLS`
+ * `BTC_ESPLORA_URLS`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 57 - 59, Update the Docker environment variables docs
in README.md to include the RPC override vars that docker-compose.yml already
passes through: ETH_RPC_URLS, BNB_RPC_URLS, SOL_RPC_URLS, and BTC_ESPLORA_URLS.
Keep the existing env list structure near BLOCKSTREAM_CLIENT_ID,
BLOCKSTREAM_CLIENT_SECRET, and HELIUS_API_KEY, and make sure the Docker section
matches the main “Environment variables” documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@README.md`:
- Around line 57-59: Update the Docker environment variables docs in README.md
to include the RPC override vars that docker-compose.yml already passes through:
ETH_RPC_URLS, BNB_RPC_URLS, SOL_RPC_URLS, and BTC_ESPLORA_URLS. Keep the
existing env list structure near BLOCKSTREAM_CLIENT_ID,
BLOCKSTREAM_CLIENT_SECRET, and HELIUS_API_KEY, and make sure the Docker section
matches the main “Environment variables” documentation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 13980ad8-e51b-4426-80f9-69ef0859e0d6

📥 Commits

Reviewing files that changed from the base of the PR and between 77284a4 and 77e21cb.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (9)
  • README.md
  • docker-compose.yml
  • package.json
  • src/server/chain-providers.ts
  • src/server/handlers/constants.ts
  • src/server/handlers/private-api.ts
  • src/server/handlers/wallet-api.ts
  • src/server/helper.ts
  • src/server/index.tsx
💤 Files with no reviewable changes (3)
  • src/server/index.tsx
  • package.json
  • src/server/handlers/wallet-api.ts

@feruzm feruzm merged commit c9d3771 into main Jul 6, 2026
2 checks passed
@feruzm feruzm deleted the feature/public-chain-providers branch July 6, 2026 15:53
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.

1 participant