Conversation
- 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>
- Restructure pool templates from `pools/{connector}.json` to `pools/{chain}/{network}.json`
- Change pool list/get API to use chain+network as primary keys, connector becomes optional filter
- Add chain field to pool add/remove requests
- Make baseSymbol/quoteSymbol optional in add request (auto-fetched from pool info)
- Update pool-service to handle new directory structure
- Remove deprecated per-connector template files
This enables better organization of pools by network, allowing users to see
all pools on a network regardless of which DEX they belong to.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…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>
- Update Meteora to use new API (dlmm.datapi.meteora.ag/pools) - Add Orca fetch-pools endpoint using api.orca.so/v2/solana/pools/search - Standardize response format across both connectors - Add FetchPoolsRequest/Response schemas in clmm-schema.ts - Remove isVerified field (not reliably available from APIs) - Add tests for both endpoints Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The /connectors/uniswap/clmm/pool-info route was returning pool.liquidity / 10^decimals for both base and quote token amounts. Uniswap V3's `liquidity` is the active virtual liquidity in sqrt-price space (~sqrt(x*y)), not a token amount, so the returned values were meaningless — for the USDM1/USDC pool the route reported 11 USDM1 and 11 trillion USDC instead of the actual ~167.6K USDM1 / ~182.1K USDC. Replace with ERC20 balanceOf(poolAddress) for token0 and token1, the same pattern used by the Orca CLMM pool-info route. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat / organize pool templates by network instead of connector
pnpm v11 blocks postinstall build scripts for native modules by default unless they are listed in allowBuilds with a boolean value. blockExoticSubdeps alone was not enough — pnpm install still failed with ERR_PNPM_IGNORED_BUILDS. Allow the native modules the gateway actually needs (bigint-buffer, secp256k1, keccak, etc.) so pnpm install completes on v11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ken-amounts fix / return real ERC20 balances in Uniswap CLMM pool-info
Node 20 reached end of life in April 2026, so the production Docker image was running on an unsupported runtime. Bump to Node 22 LTS (supported through April 2027) and update engines.node to match. With Node 22 in place, the pnpm@10 pin (added for Node 20 compatibility) is no longer needed — switch to pnpm@11 and make `pnpm install --frozen-lockfile` actually work on v11 by: - Migrating `resolutions` from package.json to `overrides:` in pnpm-workspace.yaml (yarn-style `resolutions` is not respected by pnpm v11, which caused ERR_PNPM_LOCKFILE_CONFIG_MISMATCH). - Copying pnpm-workspace.yaml into the Docker image alongside package.json and pnpm-lock.yaml so the overrides + allowBuilds config is present at install time. - Adding node_modules / dist / coverage / .git to .dockerignore so the build context is not 1GB of macOS-built artifacts that pnpm v11 then tries to purge non-interactively. Verified: `docker build` succeeds end-to-end, container runs on Node 22.22.3, bigint-buffer loads in the container, `pnpm v11 install --frozen-lockfile` finishes in ~150ms with no errors. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix / pnpm v11 compatibility issues
Simulation failures that return a non-`Custom` runtime InstructionError
(e.g. `{"InstructionError":[3,"InvalidAccountData"]}`) were parsed as
`UNKNOWN` and reported to API callers only as the generic
"Transaction simulation failed." string. The failing instruction index
and program logs — the data needed to diagnose the failure — were
discarded from the response, forcing users to dig through Gateway
server logs (see #636).
- Recognize non-`Custom` InstructionError variants (InvalidAccountData,
InvalidAccountOwner, IllegalOwner, UninitializedAccount, etc.) in
parseSolanaError, mapping each to a descriptive message.
- Extract the failing instruction index and expose it on
ParsedSolanaError.
- Add extractProgramLogs to pull the program log tail from simulation
error messages.
- Include the instruction index and program logs in the
SIMULATION_FAILED error returned by simulateWithErrorHandling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…parsing fix / solana surface InstructionError details in simulation failures
feat / chainstack-rpc-connector
Fix / Add sensible to fix throwing generic errors to trading routes
The CLMM quote-swap endpoints for Orca and Meteora were computing `price = outputAmount / inputAmount` regardless of side. On BUY this returns base/quote (since input=quote, output=base), while on SELL it correctly returns quote/base — so BUY and SELL quotes for the same pool reported prices at different scales (e.g. 0.987 vs 1.012 for a USDC/USDM1 pool). This broke any client comparing the two sides (arb checkers, mid-price calculators, etc.) and disagreed with what the Uniswap CLMM connector returns for the same conceptual quantity. Fix: - Orca `clmm-routes/quoteSwap.ts`: reconstruct `price = quote/base` in the route from the helper's `(inputAmount, outputAmount)` and the request's `side`, keeping the helper signature unchanged. - Meteora `clmm-routes/quoteSwap.ts`: invert the BUY branch's `price` expression so it returns quote/base; SELL branch already correct. - Add regression tests asserting price units stay quote/base on both sides (Orca only — Meteora has no existing quoteSwap test). Raydium CLMM already correctly branches per side (`price = in/out` on BUY, `out/in` on SELL), so no change needed there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors Meteora's `pool-info.bins[]` shape across the Orca, Uniswap, and
Raydium CLMM connectors. Passing `?binCount=N` to `/connectors/<dex>/clmm/pool-info`
returns N tick-spacing-wide bins centered on the active tick, each reporting:
{ binId, price, baseTokenAmount, quoteTokenAmount }
with token amounts derived from V3 sqrt-price math against the active
liquidity covering that bin.
Default behavior is unchanged: `binCount` defaults to 0, so existing
pool-info callers pay no extra RPC cost.
Implementation per connector
============================
Orca — `computeOrcaBinDistribution` in `orca.utils.ts`. One
`fetchAllPositionWithFilter` + `positionWhirlpoolFilter` RPC
returns every open Position; we sum liquidity per bin and convert
to amounts via `tryGetAmountDeltaA/B` from whirlpools-core.
Uniswap — `computeUniswapBinDistribution` in `uniswap.utils.ts`. Parallel
`pool.ticks(tick)` reads at every bin boundary, propagate L
outward from `pool.liquidity()` via the V3 spec, then
`SqrtPriceMath.getAmount{0,1}Delta` for token amounts.
Raydium — schema only. `binCount` is accepted on the request so the API
contract is consistent across all CLMM connectors, but bin
computation is deferred to a follow-up PR (needs
`PoolUtils.fetchMultiplePoolTickArrays` + `LiquidityMath` wiring).
Base schema
===========
`PoolInfoSchema` gains an optional `bins` array (using the existing
`BinLiquiditySchema`), and `GetPoolInfoRequest` gains an optional
`binCount` (0..401, default 0). The Meteora-specific `bins` field
moves up to the base since Meteora was already returning the same shape.
Tests
=====
- `test/connectors/orca/clmm-routes/poolInfo.test.ts`: 4 new cases
- `test/connectors/uniswap/clmm-routes/pool-info.test.ts`: 4 new cases
- `test/connectors/raydium/clmm-routes/poolInfo.test.ts`: 5 cases (new file)
Each suite covers: no binCount → no bins[]; binCount=0 → no bins[];
binCount=N → bins[] length N with shape assertions; binCount > max → 400.
For Raydium the binCount>0 case asserts the schema is accepted but
`bins` stays undefined until the impl lands.
All 23 new + existing tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Raydium half of the binCount feature added in the previous commit
— previously schema-only because the SDK wiring was non-trivial.
`computeRaydiumBinDistribution` in `raydium.utils.ts`:
- Calls `PoolUtils.fetchComputeClmmInfo` + `PoolUtils.fetchMultiplePoolTickArrays`
to pull every tick array for the pool (one getProgramAccounts).
- For each bin boundary in the window, looks up the corresponding tick via
`TickUtils.getTickArrayStartIndexByTick` + `TickUtils.getTickOffsetInArray`
and reads its `liquidityNet` (zero-defaulted for uninitialized ticks).
- Walks L outward from `pool.liquidity` at the current tick.
- Computes per-bin token amounts via `LiquidityMath.getTokenAmount{A,B}FromLiquidity`,
splitting at the active sqrtPrice when the bin straddles the current tick.
The Raydium pool-info route is wired the same way as Orca/Uniswap:
binCount=0 (default) → unchanged behavior; binCount > 0 → populated bins[].
Tests updated in `test/connectors/raydium/clmm-routes/poolInfo.test.ts`:
- omits bins[] when binCount missing
- omits bins[] when binCount=0
- returns bins[] of length N when binCount=N
- rejects binCount above the schema max
All 24 CLMM pool-info tests pass (Orca 12 + Uniswap 6 + Raydium 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
handleTransactionExecution previously returned null as soon as _transactionExecutionTimeoutMs (default 30s) elapsed, even when the tx was about to confirm in the next block. Ethereum blocks are ~12s, so a healthy tx can need a few more blocks; reporting null at 30s caused callers to treat confirmed txs as failures. Now: after the initial race against the timeout, poll getTransactionReceipt every 5s for an additional 90s before returning null. Receipt with status=0 (reverted) is still returned immediately. Also tighten approve.ts to handle the null-receipt case the timeout path can produce — previously crashed with TypeError on receipt.status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs in the Orca CLMM quote helper, surfaced on the BUY path: 1. amountOut wrong on BUY: getOrcaSwapQuote always used swapQuoteByInputToken, so "BUY 0.2 SOL" was computed as "spend 0.2 USDC" and returned ~0.00243 SOL out. Add a `side` param and use swapQuoteByOutputToken for BUY (exact-output), matching executeSwap and the Raydium/Meteora convention. BUY now returns amountOut=0.2 SOL with amountIn derived as the required quote amount. 2. priceImpactPct inflated (~676000%) on B->A swaps: the bToA branch compared 1/executionPrice (B per A) against 1/currentPrice (A per B), mixing units and inflating impact by ~currentPrice^2. Normalize executionPrice to B-per-A before comparing to currentPrice. BUY/SELL now agree (~0.04% for the same-size trade). Verified live against pool Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three related bugs in the Raydium CLMM quote/execute swap path: - priceImpactPct was extracted via Number(response.priceImpact) * 100, but priceImpact is an SDK Percent object. Number(Percent) is NaN (toString is "[object Object]") and toSignificant() already returns a percentage, so the value collapsed to 0 (or, with older code, a nonsense 99649%). Now uses response.priceImpact.toSignificant(8) with no extra scaling, on both sides. - slippage was passed as new BN(slippagePct / 100), which truncates any sub-100% slippage to 0, disabling slippage entirely. Now passes the fraction (slippagePct / 100) directly as the SDK expects. - The BUY (ExactOut) path passed the wrong baseMint (always mintB) with the amount in the output token's decimals, then reverse-engineered the input via a 1/x decimals hack (convertAmountIn). That inverted maxAmountIn so it came out *less* than amountIn once slippage worked. Now calls computeAmountIn with baseMint = output token, so amountIn/maxAmountIn return directly in the input token's units (slippage already included). Deleted convertAmountIn and the manual slippage recompute in executeSwap. Adds test/connectors/raydium/clmm-routes/quote-swap.test.ts covering BUY, SELL, and the not-found path, asserting the price-impact scaling, slippage fraction, and maxAmountIn > amountIn invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-info Fix inverted BUY price, add binCount to CLMM pool-info, extend Ethereum tx receipt polling
…adaptive-fee pools The legacy whirlpools-core swapQuoteByInputToken/swapQuoteByOutputToken WASM path panics with an opaque 'unreachable' error on adaptive-fee whirlpools (fee tier index >= 1024), even when the oracle account is fetched and passed in. Gateway then returns a 500 with no diagnostic. Repro (mainnet): - CASH/USDC 3wijQvPKm6jHQrAkfPpok5o8WjCWPm1DGG17NmeW8q1w (adaptive, classic mints) -> panic - USDM1/USDC 6U4cpqp5eBGJdL2EsuNosnjDuxGMaDzHfTYiEsuwT5Ey (adaptive, Token-2022) -> panic - SOL/USDC, USDC/USDT (ts=1), PYUSD/USDC (Token-2022 fee/hook) -> OK Replace the quote and execute-swap paths with the v4 SDK's swapInstructions, which resolves tick arrays, the oracle and Token-2022 extensions internally and quotes all five pool classes correctly (verified live via scripts/verify-orca-v4-quote-live.ts). Quote price is reported in quote/base units on both sides; BUY maps to exact-output and SELL to exact-input of the base token. Adds devnet support via setWhirlpoolsConfig. Tests: new unit suite for getOrcaSwapQuote (side mapping, price orientation, tokenB-base inversion, devnet config) and v4 happy-path execute-swap route tests (BUY/SELL balance changes, slippage bps, SDK-failure 500). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(orca): swap quote/execution panics with WASM 'unreachable' on adaptive-fee whirlpools
sync / Gateway development -> staging
rapcmia
approved these changes
Jun 16, 2026
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.
Before submitting this PR, please make sure:
A description of the changes proposed in the pull request:
Tests performed by the developer:
Tips for QA testing: