Skip to content

sync / Gateway staging -> main for Hummingbot version 2.15.0 - #651

Merged
nikspz merged 50 commits into
mainfrom
staging
Jun 16, 2026
Merged

sync / Gateway staging -> main for Hummingbot version 2.15.0#651
nikspz merged 50 commits into
mainfrom
staging

Conversation

@nikspz

@nikspz nikspz commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

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:

Tests performed by the developer:

Tips for QA testing:

smypmsa and others added 30 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>
- 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>
- Pin Dockerfile to pnpm@10 for Node 20 compatibility (fixes #632)
  pnpm v11+ requires Node.js >= 22.13
- Add pnpm-workspace.yaml with blockExoticSubdeps: false (fixes #633)
  Allows exotic subdependencies (git-hosted packages) in dependency tree

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>
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>
mlguys and others added 19 commits May 20, 2026 22:22
…parsing

fix / solana surface InstructionError details in simulation failures
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
@nikspz
nikspz merged commit fc8046e into main Jun 16, 2026
7 checks passed
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.

5 participants