Skip to content

feat/chainstack-rpc-connector#623

Open
smypmsa wants to merge 12 commits intohummingbot:developmentfrom
smypmsa:feat/chainstack-rpc-connector
Open

feat/chainstack-rpc-connector#623
smypmsa wants to merge 12 commits intohummingbot:developmentfrom
smypmsa:feat/chainstack-rpc-connector

Conversation

@smypmsa
Copy link
Copy Markdown

@smypmsa smypmsa commented Apr 15, 2026

Before submitting this PR, please make sure:

  • Your code builds clean without any errors or warnings
  • You are using approved title ("feat/", "fix/", "docs/", "refactor/")

A description of the changes proposed in the pull request:

Adds Chainstack as a third RPC provider for both Solana and EVM chains, alongside Helius and Infura. Chainstack is the first cross-chain provider for the gateway and the first to use API-driven node discovery instead of URL templating: it calls GET https://api.chainstack.com/v1/nodes once at init and matches the caller's chain/network to a deployed node's https_endpoint / wss_endpoint.

Coverage: 100% of gateway-supported networks — Ethereum, Arbitrum, Polygon, Optimism, Base, Avalanche, BSC, Celo, Sepolia, Solana mainnet, Solana devnet — plus 50+ more available on the platform.

Setup for users:

  1. apiKeys.chainstack: '<key>' in apiKeys.yml
  2. rpcProvider: chainstack in chains/solana.yml and/or chains/ethereum.yml
  3. (optional) preferredNodeId: 'ND-...' in rpc/chainstack.yml to pin a specific deployment

The PR is split into 4 commits for review:

  1. Config templates, schemas, apiKeys entry, copy-files glob, live test script
  2. ChainstackService (extends RPCProvider) + 18 unit tests
  3. Wire Chainstack into the Solana and Ethereum chain constructors
  4. Surface the discovered URL in /chains/ethereum/status and the startup banner

Tests performed by the developer:

  • pnpm typecheck — clean
  • pnpm build — clean
  • pnpm lint — 0 errors (259 warnings, same baseline as development)
  • pnpm jest --runInBand test/rpc/ test/chains/ethereum test/chains/solana test/services268/268 pass
  • ChainstackService has 18 dedicated unit tests covering every public method, the network mapping, preferredNodeId selection, and every error path (invalid API key, no matching node, stopped node, unmapped chain)
  • Verified the protocol/network mapping table against the live Chainstack Platform API:
    • All 11 rows match get_deployment_options (the authoritative source)
    • Spot-checked with create_node → get_node → delete_node for an EVM chain (arbitrum) and Solana — fields echo back exactly
    • Hit a real https_endpoint with getSlot → returns a valid slot, confirming the URL Chainstack hands out is usable as-is
  • Production code uses httpGet / httpPost from src/services/http-client — no axios in src/, consistent with refactor / replace axios with native fetch for HTTP requests #621

Tips for QA testing:

  1. Get a Chainstack API key from https://console.chainstack.com/user/settings/api-keys
  2. Deploy a node via the Chainstack console (or the Chainstack MCP) for any gateway-supported network
  3. Edit conf/apiKeys.yml: chainstack: '<your key>'
  4. Edit conf/chains/solana.yml (or ethereum.yml): rpcProvider: chainstack
  5. Start the gateway — the startup banner should log Using Chainstack RPC URL: ... with the discovered endpoint
  6. Hit GET /chains/solana/status?network=mainnet-beta (or the ethereum equivalent) — rpcUrl in the response should match the Chainstack endpoint, rpcProvider should be chainstack
  7. Execute a swap (Jupiter / Raydium / Uniswap) — Solana confirmation flows through the WebSocket monitoring path automatically
  8. To test the fallback path: set rpcProvider: chainstack but leave the API key empty — the gateway should log a warning and fall back to nodeURL
  9. Optional: pin a specific node by setting preferredNodeId: 'ND-...-...-...' in conf/rpc/chainstack.yml

smypmsa and others added 9 commits April 15, 2026 10:46
- Add src/templates/rpc/chainstack.yml (preferredNodeId only;
  the API key lives in apiKeys.yml as the single source of truth)
- Add chainstack-schema.json + register chainstack namespace in root.yml
- Add chainstack to apiKeys.yml + apiKeys-schema.json
- Add 'chainstack' to the rpcProvider enum in solana-chain-schema.json
  and ethereum-chain-schema.json
- Include src/templates/rpc/*.yml in the build copy-files glob
- Add scripts/test-chainstack-live.js for live integration testing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ChainstackService extends RPCProvider and discovers nodes via the
Chainstack Platform API (GET /v1/nodes), mapping the caller's gateway
chain/network to the deployed node's https/wss endpoints.

- Solana: WebSocket signatureSubscribe monitoring (HeliusService pattern)
- Ethereum: StaticJsonRpcProvider wrapped with the rate-limit interceptor
- Optional preferredNodeId pins a specific deployment
- Uses httpGet/httpPost from src/services/http-client (no axios in src)

Includes 18 unit tests covering every public method, the network
mapping, preferredNodeId selection, and error paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add an 'else if (rpcProvider === \"chainstack\")' branch in both chain
  constructors, mirroring the helius/infura pattern
- Initialize ChainstackService synchronously with a nodeURL placeholder
  connection/provider; swap to the discovered Chainstack URL after the
  async getInstance().init() resolves
- On Chainstack discovery failure, log a warning and fall back to
  nodeURL (consistent with the existing helius/infura fallback)
- Expose getChainstackService() getter on Ethereum, mirroring
  getInfuraService()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both /chains/ethereum/status and the startup banner had hardcoded
helius/infura branches to display the real RPC URL. Without a
chainstack branch they would have shown the bare nodeURL even when
Chainstack was the active provider.

- ethereum/routes/status.ts: add chainstack branch using
  ethereum.getChainstackService()?.getHttpUrl()
- startup-banner.ts: pick up the discovered URL after getInstance()
  resolves (Chainstack discovers its endpoint asynchronously, unlike
  Helius/Infura which can be constructed synchronously)

The Solana status route already used solana.getRpcProviderService()
generically, so no change was needed there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Chainstack node endpoints have the shape
https://<subdomain>.core.chainstack.com/<token> where the path
segment IS the per-node access credential. The previous redactUrl()
regexes only matched ?api-key= query strings (Helius) and
/v3/<32-char> paths (Infura), so the new chainstack code paths in
this PR were emitting the full credential-bearing URL into the
winston file transport on every gateway start.

- Add a Chainstack/p2pify pattern to redactUrl()
- Add unit tests covering all three provider URL formats plus
  negative cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Helius and Infura don't have their own namespaces — they only use the
centralized apiKeys.yml entry plus the rpcProvider chain setting.
Chainstack shouldn't need one either.

- Delete src/templates/rpc/chainstack.yml and chainstack-schema.json
- Remove $namespace chainstack from root.yml
- Remove preferredNodeId from ChainstackService (always auto-selects
  the first running node matching the chain/network)
- Revert the copy-files rpc/*.yml glob addition in package.json
- Remove the two preferredNodeId test cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Live e2e testing revealed the Chainstack Platform API differs from
the original plan's assumed shape:

- GET /v1/nodes returns paginated { results: [...] }, not a bare array
- Node objects carry a network ID (e.g., "NW-056-237-8"), not a name
- Endpoints are nested under `details` with a separate `auth_key`
- Protocol/network name must be resolved via GET /v1/networks/{id}

Changes:
- Add ChainstackApiNode, ChainstackNodesResponse, ChainstackApiNetwork
  interfaces matching the real API
- Add resolveNetwork() to fetch protocol+network from network IDs
- Rewrite initialize() to handle pagination, resolve networks, and
  build authenticated endpoint URLs (base + auth_key)
- Cache resolved networks to avoid duplicate lookups
- Update all 16 test cases with real-shaped fixtures
  (mkApiNode, mkNetworkResponse, mockNodesAndNetworks helper)

Verified end-to-end against a live Solana mainnet Chainstack node:
  ChainstackService.initialize() → discovered node ND-899-167-294
  → healthCheck() passed (getSlot)
  → Connection.getSlot/getBlockHeight/getBalance/getLatestBlockhash OK
  → WSS slotSubscribe OK

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@fengtality
Copy link
Copy Markdown
Contributor

Thanks for this PR — solid work overall. Two things worth addressing before merge:

1. instanceof ChainstackService check in solana.ts

The URL swap after init() only triggers via an instanceof check, which leaks the concrete type into the chain layer. The cleanest fix is to add a getHttpUrl() method to the RPCProvider base class (returning null by default) and always swap the connection when it returns a value — removes the special-casing.

2. Duplicated network map

ChainstackService.NETWORK_MAP and GATEWAY_NETWORKS in scripts/test-chainstack-live.js are manually kept in sync (the script even notes this). The test script should import the constant from the service directly to avoid future drift.

Minor (non-blocking):

  • getChainstackService() on Ethereum vs getRpcProviderService() on Solana is an inconsistent public API between the two chains — worth aligning
  • preferredNodeId should be mentioned in the main config schema or env var docs so users can discover it

smypmsa and others added 3 commits April 28, 2026 22:34
…lana

Per PR review, the chain layer was using `instanceof ChainstackService` to
decide whether to swap the Solana connection post-initialize. The cleaner
shape is a polymorphic getHttpUrl on the base.

- RPCProvider.getHttpUrl now returns string | null (default null) instead of
  being abstract returning string. Subclasses override to return their URL
  whenever it is available.
- ChainstackService.getHttpUrl returns null pre-initialize (no throw).
- Helius/Infura get an `override` keyword (signature unchanged).
- solana.ts no longer special-cases Chainstack: after initialize(), if
  getHttpUrl() yields a URL, swap the connection.
- Status routes and redactUrl handle null safely.
- Test that asserted pre-init throw flipped to expect null.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orks

The live script used to redeclare the supported (chain, network) pairs in
GATEWAY_NETWORKS and noted in a comment that it was kept in sync by hand.
Per PR review, drift-prone.

- ChainstackService.getSupportedNetworks() exposes the NETWORK_MAP keys.
- scripts/test-chainstack-live.js converted to .ts and imports the static
  list directly. expectedChainId stays in the script as gateway-side
  metadata, since chainIds are not Chainstack's concern.
- Run with: npx ts-node scripts/test-chainstack-live.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per PR review, getInfuraService/getChainstackService on Ethereum vs
getRpcProviderService on Solana was an inconsistent public API. Now that
getHttpUrl lives on the RPCProvider base, the typed getters add no value.

- Ethereum exposes a single polymorphic getRpcProviderService() returning
  RPCProvider | null. getInfuraService and getChainstackService removed.
- Private fields infuraService and chainstackService kept inside ethereum.ts
  since their setup paths differ.
- Ethereum status route is provider-agnostic now, mirroring Solana.
- startup-banner uses the unified getter.
- Status route tests updated to mock getRpcProviderService.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@smypmsa
Copy link
Copy Markdown
Author

smypmsa commented Apr 28, 2026

@fengtality Thanks for the review, addressed all four:

1. getHttpUrl() now lives on the RPCProvider base, returning string | null (default null). ChainstackService overrides to return its discovered URL or null pre-init; Helius/Infura keep returning their templated URL. solana.ts (and ethereum.ts) now always swap the connection when getHttpUrl() returns a non-null value, no more instanceof. Commit: 71a5a50.

2. Added ChainstackService.getSupportedNetworks() as the single source of truth and converted scripts/test-chainstack-live.js to .ts so it imports the static list directly. expectedChainId stays in the script as gateway-side metadata. Commit: 382814b.

3. Replaced getInfuraService() and getChainstackService() on Ethereum with a single polymorphic getRpcProviderService(): RPCProvider | null, symmetric with Solana now. Status routes are provider-agnostic on both chains. Commit: 5de27b9.

4. preferredNodeId was removed in 3b6f7ec per your earlier feedback (you asked us to drop the chainstack namespace, since Helius/Infura don't have one). It's gone from the source, schema, and tests, nothing left to document. If you still see it in your local conf/rpc/chainstack.yml, that's a stale artifact from before the deletion.

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