fix(clmm): retry-ownership gateway side, unified poll status contract, binCount on pool-info - #679
fix(clmm): retry-ownership gateway side, unified poll status contract, binCount on pool-info#679fengtality wants to merge 31 commits into
Conversation
…ard; orca position-info contract Gateway-side changes for the gateway#678 retry-ownership work (see docs/retry-architecture.md, included here as the canonical cross-repo design): - solana-error-parser: map Orca Whirlpool 6018 TokenMinSubceeded to SLIPPAGE_EXCEEDED, and attribute custom program errors to the program on the "failed: custom program error" log line instead of the first "invoke" line — simulation-shaped errors open with ComputeBudget, so the DEX-specific error tables were never consulted (the actual #678 MATH_OVERFLOW misreport mechanism). Regression-tested with a full simulation-shaped log. - solana: reject transactions whose compute-estimation simulation returned an error, in both send paths — stale-state failures become a typed 400 before broadcast (zero fees) instead of a broadcast failure. - orca: getPositionInfo returns null ONLY when fetchMaybePosition reports the account does not exist; transient errors now propagate. Callers treat null as "position closed", so a swallowed RPC blip could abandon a live funded position while reporting success. Deliberately NOT included: the in-route close retry loop from 040e99e. Gateway stays a stateless transaction oracle — one request, one attempt, typed errors; retry ownership lives in the Hummingbot connector/executor (see the doc, §6). Validated live on mainnet: forced-failure cascade (fault-injected minimums) had every doomed close rejected pre-broadcast at zero fee cost across 33 attempts, with 6018 correctly surfaced as SLIPPAGE_EXCEEDED. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HahKfEY9rvKnZijrzUAFSq
Greptile SummaryThe PR standardizes transaction polling and typed Solana failures, rejects failed simulations before broadcast, extends unified CLMM pool information with configurable bins, and updates connector transaction interfaces.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains within the scope of the available prior review threads. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| src/chains/solana/solana.ts | Adds simulation failure guards and signature-status classification for Solana transaction polling. |
| src/chains/solana/solana-error-parser.ts | Attributes custom program errors to the failing program and expands typed Whirlpool error handling. |
| src/chains/ethereum/routes/poll.ts | Replaces chain-specific polling heuristics with the shared transaction-status contract and correctly distinguishes reverted receipts. |
| src/connectors/orca/orca.ts | Narrows absent-position handling so transient position-fetch failures propagate to callers. |
| src/trading/clmm/pools.ts | Accepts and forwards the optional CLMM bin count while preserving Meteora’s connector-specific behavior. |
| src/connectors/clmm-v3-utils.ts | Centralizes bigint-based V3 tick traversal and bin-distribution calculations for compatible connectors. |
| src/connectors/pancakeswap/clmm-routes/poolInfo.ts | Adds PancakeSwap bin calculation and reports actual pool token balances. |
| src/schemas/chain-schema.ts | Defines the common transaction-status codes consumed by both chain polling routes. |
| openapi.json | Regenerates the public API contract for the revised schemas and route parameters. |
| docs/retry-architecture.md | Documents cross-repository retry ownership, terminal semantics, polling behavior, and CLMM pool-information flow. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Client[Gateway client] --> Trading[Unified trading routes]
Trading --> Connector[Chain or DEX connector]
Connector --> Simulation[Build and simulate transaction]
Simulation -->|simulation succeeds| Broadcast[Broadcast and confirm]
Simulation -->|simulation fails| TypedError[Typed pre-broadcast error]
Broadcast --> Poll[Unified transaction polling]
Poll --> Status[NOT_FOUND / FAILED / PENDING / CONFIRMED]
Trading --> PoolInfo[CLMM pool information]
PoolInfo --> Bins[Optional bin distribution]
Reviews (20): Last reviewed commit: "test(jupiter): assert removed fields' ab..." | Re-trigger Greptile
getTransaction (commitment 'confirmed') returns null both for a transaction awaiting confirmation and for one the cluster has never seen, so /poll reported txStatus 0 (pending) for dropped transactions forever — pollers had no signal to stop waiting on a transaction that can never land once its blockhash expires. The poll route now consults getSignatureStatuses (with history search) when txData is null: a signature the cluster has seen stays UNCONFIRMED (0); an unknown signature returns the new NOT_FOUND (-2), as does a malformed signature. -2 avoids colliding with the Ethereum poll's existing 2/3 mempool heuristics. Transient RPC errors still report UNCONFIRMED so callers keep polling rather than giving up on an unknown outcome. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the design-evolution narrative (proposals, verdicts, review logs, deployment diaries) with a clean description: the ownership principle, the sixteen issues found across the four repos, and the architecture as it now stands — layered ownership, close-vs-open asymmetry, the close lifecycle, terminal semantics, the two topologies with the orphan lifecycle, and the bounded transaction-status polling contract (including the new NOT_FOUND poll status). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reverts as FAILED
The two poll routes spoke different dialects: Solana used a local enum
(-1/0/1), Ethereum used raw numbers including 2 ('likely to be processed')
and 3 ('likely stuck') that no consumer understood, reported not-found as -1
after blocking the request for three 1-second in-route retries, and — via
'typeof receipt.status === number ? 1 : -1' — reported REVERTED transactions
(status 0, which is a number) as CONFIRMED, so a reverted swap polled as
filled.
Both routes now share TransactionStatusCode in chain-schema:
NOT_FOUND (-2) / FAILED (-1) / PENDING (0) / CONFIRMED (1).
Ethereum: not-found returns -2 immediately (no in-route sleeps — the caller
owns pacing and the not-found deadline), mempool is plain PENDING (gas-price
heuristics dropped), and receipt status 0 maps to FAILED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Updates the retry-architecture doc for the two follow-up changes: the connector's retryable-code opt-in and inner budget are gone (the executor's CLOSING re-entry with max_retries=0 per attempt is the only close retry loop), and both chains' poll routes now share one TransactionStatusCode contract — including the Ethereum findings (2/3 heuristics, in-route retry sleeps, reverts reported as confirmed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Found in live testing: /poll parsed only JSON.stringify(meta.err), which carries the error code but names no program — so extractProgramId never matched, every program-specific code fell through to the generic map, and a confirmed-but-failed Orca transaction reported 'UNKNOWN (0x1782)' instead of SLIPPAGE_EXCEEDED. Same misreporting as gateway#678, on the async path. The err object is now parsed together with meta.logMessages, whose 'Program X failed: custom program error' line is what the parser attributes on. Errors raised by programs with no registered table (e.g. a third-party router that CPIs into Whirlpool) correctly stay generic rather than being misattributed to the DEX they called. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Swap GET /trading/clmm/pool-info could never return `bins`: its querystring schema had no binCount and it called every connector as (fastify, network, poolAddress), dropping the parameter that orca/raydium/uniswap already support. Since hummingbot-api and condor read pool info through the unified route, the bin distribution was unreachable outside the per-connector routes. - Unified route accepts binCount and forwards it. Meteora is called without it, as it always returns its own bins. - PancakeSwap CLMM gains binCount. The V3 tick walk moves to a shared clmm-v3-utils helper: the two SDKs disagree on numeric type (@uniswap/v3-sdk is JSBI, @pancakeswap/v3-sdk is native bigint), so the helper works in bigint and each connector adapts its own SDK rather than importing the other's. - Fixes PancakeSwap pool-info token amounts, which were pool.liquidity (V3 virtual liquidity in sqrt-price space) scaled by each token's decimals — reporting the same meaningless figure for both sides. Now ERC20 balanceOf on the pool contract, the same fix Uniswap already carries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
binance.llamarpc.com is dead — it answers nothing, so every BSC read failed (pancakeswap pool-info reported 'Pool not found' for pools that exist). bsc-dataseed.bnbchain.org is BNB Chain's official public endpoint and returns chainId 0x38. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eth.llamarpc.com was unreachable — gateway logged 'Unable to fetch block number' on every startup and all mainnet reads failed. eth-mainnet.g.alchemy.com/public returns chainId 0x1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The audit that #678 triggered found defects on two read paths the LP flow depends on but that the issue never named — the transaction-status contract the poller reads, and the pool-info contract the dashboard and agents read. Both belong here: they share #678's root cause, a caller unable to tell a definitive answer from a transient one, or unable to ask for what it needs. Adds the CLMM pool-info issues (binCount unreachable through the unified route, PancakeSwap missing bins and reporting virtual liquidity as token amounts, Raydium bypassing Gateway, two dead default RPCs) and a section describing the request chain, the per-connector cost of binCount, and the bin output shape. Notes the fifth repository now in the family. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The CLMM work on this branch depends on Gateway changes that ship in hummingbot/gateway#679 and are not in the `latest` tag, so a container started from the default image cannot serve the endpoints this branch calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
add.ts rejects a deposit with neither amount positive at the route level; open let the same body run into connector code before failing. Apply the identical guard (single-sided opens stay valid). The unsupported-connector test gains an amount so it still exercises connector routing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
rewardTokenAddress/rewardAmount had no producer — the only assignments (pancakeswap-sol) are commented out — so every consumer saw permanently absent optionals. Removed from the schema; hummingbot-api drops its passthrough in step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…nge amounts binCount on the unified /trading/clmm/pool-info now works for every connector except Meteora (which always returns its bins): pancakeswap-sol was the one connector ignoring it. New computeBinDistribution mirrors the raydium/orca walk against the fork's identical TickArrayState layout — tick-array PDAs fetched in one getMultipleAccountsInfo, liquidity_net (i128) propagated outward from the active bin. Verifying the bins exposed a real connector bug: getAmountsFromLiquidity had its out-of-range branches swapped (price below a range put the liquidity in token1 instead of token0, and vice versa), so bins — and out-of-range position-info and quote-position amounts — came back inverted. In-range amounts were always correct, which is why it went unnoticed. Verified live on the SOL-USDC pool: below-price bins now hold quote, above-price bins hold base, matching raydium and orca exactly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…hemas
/trading/clmm/create-pool shrinks to three connector params, each meaning
the same thing everywhere:
- binStep: bin/tick granularity (Meteora DLMM bin step; Orca tick spacing)
- feeBps: base fee in basis points (Meteora base fee; Uniswap/PancakeSwap
V3 tier mapped x100 to the protocol's hundredths-of-a-bip units, with a
bps-worded required guard)
- ammConfigIndex: Raydium-family fee-config index — Raydium API list
index, and pancakeswap-sol amm_config PDA index (["amm_config", index],
big-endian with little-endian fallback, validated on-chain) replacing
the raw ammConfig address parameter
Dropped: fee, tickSpacing, ammConfig, gasPrice, maxGas.
The request and response are now canonical in clmm-schema.ts; the unified
route composes its body from ClmmCreatePoolRequest. The response drops
the AMM-inherited baseTokenAmountAdded/quoteTokenAmountAdded — every
connector hardcoded them to 0 because CLMM create-pool initializes an
empty pool — leaving data = { fee }.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…ig-index name - gasPrice/maxGas removed from the unified /trading/amm/create-pool and from the uniswap/pancakeswap connector routes (add/remove-liquidity, create-pool request schemas): gas is configured at the network level, and the EVM connectors were the only ones exposing per-request overrides. Transactions now use prepareGasOptions(undefined, <route gas limit>). - feeConfigIndex renamed ammConfigIndex on the unified AMM create-pool, matching the CLMM create-pool vocabulary for the Raydium-family fee-config index. configAddress stays: DAMM v2 configs are permissionless accounts with no index derivation. - The unified AMM create-pool body is now composed from the canonical CreatePoolRequest (amm-schema.ts), like its CLMM counterpart. - slippagePct on every unified AMM route now declares default 1 (with the same description/examples as the CLMM routes) — swagger previously rendered the bare maximum (100) as the example, which read as a 100% default slippage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…NoExactOut only /trading/swap/quote and /trading/swap/execute now expose exactly two optional knobs beyond the core fields: slippagePct and approximateIfNoExactOut (default true; for BUY orders when a router has no ExactOut route, approximate via a sell-leg ExactIn quote instead of failing). approximateIfNoExactOut is threaded through the unified dispatch and supported by all four Solana routers — jupiter, dflow, okx and titan — each keeping its refuse-with-explanation path when a caller passes false. EVM routers quote ExactOut natively and ignore it. Removed from request surfaces, per the config-over-request principle: - 0x gasPrice/maxGas (gas is network-level; tx gas limit comes from the 0x quote's own estimate) - jupiter restrictIntermediateTokens/onlyDirectRoutes (routing policy, connector config) and priorityLevel/maxLamports (Solana priority fees, connector config — the gas knobs of Solana) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…l the schema openTime was Raydium CPMM's scheduled trading-start (startTime) — a fair-launch nicety only one connector reads, not something needed to create a pool; the connector defaults it to open-immediately. The composite now orders required fields first (connector, chainNetwork, walletAddress, canonical create fields) with the optional per-protocol fee-config selectors last. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
The Meteora connector's addLiquidity always needed a DLMM distribution shape and fell back to the connector-config default when the unified route didn't pass one — so a position opened as one shape through /trading/clmm/open silently accreted liquidity in a different shape on /trading/clmm/add. Same optional Meteora-only parameter as open; other connectors ignore it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
- expose slippagePct on /trading/clmm/remove (orca-only; was hardcoded 1) - pass slippagePct through to pancakeswap-sol in /trading/clmm/quote-position - drop slippagePct||1 coercion for uniswap/0x router quotes (honors explicit 0, falls back to connector config) - enum-constrain connector on every unified route so unknown connectors 400 at the schema; refresh stale connector lists in descriptions - default percentageToRemove to 100 on /trading/amm/remove-liquidity, matching CLMM - walletAddress uniformly required-with-default - hoist parseChainNetwork/defaultWallet/connector+chainNetwork fields into src/trading/common.ts (malformed chainNetwork now 400s everywhere) - remove legacy tradingRoutes alias; app registers tradingSwapRoutes directly Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
- new rethrowRouteError in trading/common.ts: errors with an HTTP statusCode (connector badRequest/notFound, chain errors) propagate untouched; anything else becomes a 500 that keeps the underlying message instead of a generic label - applied to all 21 unified swap/clmm/amm route handlers (tx routes previously swallowed the cause; query routes rethrew raw; swap routes had their own wrapper) - open.ts: consolidate the mid-file import block at the top — the eslint hook had sorted the ../common import below the schema const that uses it at module evaluation, breaking require-order-sensitive loads Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…everywhere - unified routes drop the schema-level slippagePct default (Fastify injected it before the handler, shadowing connector defaults): omitted slippage now falls through to each connector's configured slippagePct, fallback 1 - orca: replace hardcoded '= 1' slippage defaults with OrcaConfig.config.slippagePct ?? 1 across quoteSwap/executeSwap/quotePosition/ openPosition/addLiquidity/removeLiquidity; remove-request schema default now config-driven too - meteora/orca CLMM remove-liquidity: rename liquidityPct -> percentageToRemove, matching every other connector, the unified routes, and meteora's own AMM route. The old name let clients sending percentageToRemove (hummingbot's gateway_http_client does) silently remove 100% via the schema default. - jupiter swap test: assert the current execute-swap surface (slippagePct + approximateIfNoExactOut) instead of the removed priority-fee fields Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
Snapshot was ~318 commits stale — still carried liquidityPct on the meteora/orca CLMM remove routes and the removed schema-level slippagePct defaults on the unified trading routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
A single not.objectContaining({a, b}) passes when either key is missing;
check each removed priority-fee field on the actual call body instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…oute /trading/swap/quote|execute already dispatches by connector/type (jupiter/router, raydium/amm, meteora/clmm, ...) and resolves the pool internally, so the pool-scoped /trading/amm/quote-swap and execute-swap duplicated it with a narrower surface. Wire the missing meteora/amm branch into the unified route and delete the pool-scoped pair; callers (order executor included) always go through /trading/swap regardless of connector type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…ed slippagePct Three defect clusters: - pancakeswap-sol (G3): all six clmm routes now call the shared solana.throwIfLandedWithError before returning PENDING, so a transaction that landed on-chain and failed throws 400 TRANSACTION_FAILED instead of being reported as status 0 forever. collectFees no longer removes 1% of the position's liquidity and reports the removed amounts as "fees" — the program (a Raydium CLMM fork) collects owed fees via decrease_liquidity_v2, so the route now sends a zero-liquidity decrease that collects the real fees without touching liquidity. - Solana route confirmation (G4): retire the `txData !== null` == confirmed pattern. New Solana.getConfirmedTransactionData(signature) does the route-level re-fetch with retry (a just-confirmed tx whose data lags RPC visibility is no longer misreported as PENDING) and throws the shared landed-but-failed error on meta.err; every meteora/raydium/orca route and the jupiter/dflow/okx/titan executeQuote paths use it. handleConfirmation now takes raw txData (no confirmed flag), checks meta.err itself, and re-fetches with retry when no data is passed. throwIfLandedWithError is public, accepts optional txData, and throws the new 400 TRANSACTION_FAILED (error-handler) instead of mislabeling a landed tx as SIMULATION_FAILED. - slippagePct echo (D4): the four swap-execute response schemas (chain/router/amm/clmm) gain an optional data.slippagePct, populated by every executeSwap/executeQuote implementation (Solana amm/clmm/router and uniswap/pancakeswap/0x) with the slippage actually applied — the request value when given, else the connector's configured default. quote-cache gains getRequest() so 0x can recover the applied value at execution. Tests: new confirmation-helpers suite pins the retry/meta.err/slippage contract; new pancakeswap-sol collectFees suite pins zero-liquidity collect and the loud landed-but-failed 400; meteora/okx suites extended for the slippagePct echo and serializer survival. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
The unified swap route resolves a pool from the configured pool list by token pair, which a pool that is not in that list cannot be — a freshly created pool, or one on an unlisted token. Folding the pool-scoped AMM swap routes into it therefore left those pools unreachable. An optional poolAddress restores the pin on the one surface: amm/clmm providers trade against it directly, routers reject it since they choose their own route, and the not-found message now says the pin exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…emoves The orca remove-liquidity tests sent 'percentage' and the unified CLMM route tests sent 'network' — both were renamed (percentageToRemove, chainNetwork) and Fastify silently dropped the stale keys, so every case ran against schema defaults instead of the values under test. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
The spec still published /trading/amm/quote-swap and execute-swap, which were folded into the unified swap route, and carried neither the poolAddress pin nor the applied-slippage echo. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
|
Too many files changed for review (162 files, 100 file limit). Bypass the limit by tagging |
collectFees removed 1% of the position and reported the withdrawn principal as fees — mutating the position on a read-shaped verb and mis-stating the amounts. The Raydium CLMM program transfers owed fees on any decrease_liquidity, so a zero-liquidity decrease collects them and leaves the principal intact, matching the pancakeswap-sol fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
Ethereum.handleTransactionExecution returns null for a transaction still
pending after its timeout plus a 90s extended poll, and a receipt with
status 0 for one that reverted. Only the router executeQuote path handled
either; every other EVM route dereferenced the receipt straight away or
forwarded receipt.status verbatim.
New Ethereum.handleTransactionConfirmation(tx) is the single gate. It wraps
handleTransactionExecution and collapses three outcomes into the two a
response body can express:
- still pending -> { confirmed: false, signature: tx.hash }; the caller
returns { signature, status: PENDING } with NO data.
- reverted -> throws the shared 400 TRANSACTION_FAILED (error-handler),
the same terminal error the Solana routes throw for landed-but-failed.
- confirmed -> { confirmed: true, signature, receipt, fee }, gas fee
already in native units.
Three defect clusters, mirroring the Solana remediation in 828159e:
- Null receipt lost the transaction (E1): 21 uniswap/pancakeswap amm+clmm
routes read receipt.logs / receipt.gasUsed / receipt.status with no null
check, so a pending transaction threw a TypeError that the route catch
turned into a generic 500 — discarding the hash, so a caller could never
reconcile a transaction that landed a minute later. All now return the
pending shape with the hash. uniswap clmm openPosition's catch no longer
swallows the underlying message behind a bare 'Failed to open position'.
chain routes wrap/unwrap had the same crash and are fixed with it.
- Revert reported as PENDING (E2): the liquidity routes returned the raw
receipt.status, and TransactionStatus.PENDING is also 0 — so a reverted
remove/close was reported as "still pending" while its data claimed the
pre-send amounts had been withdrawn. Downstream pollers wait on such a
transaction forever and book tokens that never moved. Throwing 400
TRANSACTION_FAILED (rather than status -1) matches what the Solana
liquidity routes now do and what the EVM swap routes already did loosely;
the swap routes' generic 500 'Transaction reverted on-chain' becomes the
same typed error. The two create-pool families stopped labelling reverts
as pending, and approve's `approval.status ?? -1` — which also reported a
confirmed Ledger approval as FAILED — is gone.
- Unaddressable position (E3): uniswap and pancakeswap clmm openPosition set
positionId = '' when no NFT-mint Transfer log was found and still returned
a CONFIRMED response carrying data.positionAddress: ''. They now throw a
500 naming the transaction so the position stays recoverable.
The router executeQuote paths (uniswap/pancakeswap/0x) are unchanged — they
already go through handleExecuteQuoteTransactionConfirmation, the reference
implementation this gate was modelled on. openapi.json is unchanged: no
response schema moved, and `data` was already Optional everywhere.
Tests: new suites pin the gate itself (pending preserves the hash, revert
throws TRANSACTION_FAILED, confirmed carries the receipt fee) and the same
contract end-to-end over a CLMM route (collect-fees), an AMM route
(remove-liquidity) and open-position, including the loud failure when a
confirmed open has no mint log and the un-swallowed catch message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
|
Superseded — reopening against the current branch. The work continues in a new PR, linked from the same cross-repo set; condor #204 stays open and is referenced there. |
The CLMM work on this branch depends on Gateway changes that ship in hummingbot/gateway#679 and are not in the `latest` tag, so a container started from the default image cannot serve the endpoints this branch calls. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
Summary
Gateway-side changes for the gateway#678 LP-close retry-ownership work, plus the CLMM pool-info and transaction-poll fixes found while validating it live. This PR carries the canonical cross-repo design doc:
docs/retry-architecture.md— the reference for all four companion PRs.Fixes #678 (gateway side; the retry itself lives in the Hummingbot PR below).
Typed errors and fail-fast (the #678 mechanism)
6018 TokenMinSubceeded→SLIPPAGE_EXCEEDED, and attribute custom program errors to the program on thefailed: custom program errorlog line instead of the firstinvokeline. Simulation-shaped errors open with a ComputeBudget prelude, so the DEX-specific error tables were never consulted — this is the actual mechanism behind Orca close-position should rebuild and retry after transient failures #678'sMATH_OVERFLOWmisreport.getPositionInfocontract (ported onto the feat(orca): migrate connector to current Whirlpools SDK #676 SDK migration): returnsnullonly whenfetchMaybePositionreports the account does not exist; transient errors propagate. Callers treatnullas "position closed", so a swallowed RPC blip could abandon a live funded position while reporting success.slippagePct(~1%), the exact condition under which Orca close-position should rebuild and retry after transient failures #678 was reachable (the legacy route used a 50% buffer).One transaction-status contract for both chains
The two
/pollroutes spoke different dialects, and both had defects that made a poller unable to act on the answer:txStatus 0(pending) for a signature the cluster had never seen — indistinguishable from one awaiting confirmation, so a dropped transaction polled as pending forever. It now consults the signature-status cache and reports the newNOT_FOUND(-2), which is terminal once the transaction's blockhash expires.typeof receipt.status === 'number' ? 1 : -1, and a revert's status is0, which is a number. It also emitted2/3gas-price heuristics no consumer understood, and blocked the request for three 1-second retries before reporting not-found as-1.TransactionStatusCode:NOT_FOUND (-2) / FAILED (-1) / PENDING (0) / CONFIRMED (1). Transient poll errors reportPENDING— an unknown outcome is a reason to poll again, not to give up./pollparsed onlyJSON.stringify(meta.err), which carries the error code but names no program, so every program-specific code fell through toUNKNOWN. It now parses the err together withmeta.logMessages. Errors raised by programs with no registered table (e.g. a third-party router that CPIs into Whirlpool) correctly stay generic rather than being misattributed to the DEX they called.CLMM pool-info:
binCountGET /trading/clmm/pool-info— the unified route hummingbot-api and condor read through — could never returnbins: its querystring schema had nobinCountand it called every connector as(fastify, network, poolAddress), dropping the parameter that orca/raydium/uniswap already supported.binCountand forwards it; Meteora is called without it, as it always returns its own bins.clmm-v3-utilshelper — the two SDKs disagree on numeric type (@uniswap/v3-sdkis JSBI,@pancakeswap/v3-sdkis nativebigint), so the helper works inbigintand each connector adapts its own SDK rather than importing the other's math.pool.liquidity(V3 virtual liquidity in sqrt-price space) scaled by each token's decimals — the same meaningless figure reported for both sides. Now ERC20balanceOfon the pool contract, the fix Uniswap already carried.Dead default RPCs
eth.llamarpc.com(mainnet) andbinance.llamarpc.com(BSC) answer nothing — gateway logged "Unable to fetch block number" at startup and every read failed, which is why PancakeSwap pool-info reported "Pool not found" for pools that plainly exist. Defaults are noweth-mainnet.g.alchemy.com/public(chainId0x1) andbsc-dataseed.bnbchain.org(chainId0x38).Companion PRs
docs/retry-architecture.md(in the gateway PR)binCounton unified CLMM pool-infoPOSITION_HOLD, fresh position reads, bounded pending-tx pollingbin_countpassthrough, Raydium routed through Gatewaybin_countonget_pool_info(1.5.8)bin_countonget_pool_infoValidation
tsc+ eslint clean; 257 chain tests and 181 connector/trading tests pass, including new coverage for both poll routes (incl. a regression test pinning EVM reverts toFAILED), thebinCountpassthrough, and PancakeSwap pool-info.Validated live on mainnet with the stack deployed from these branches:
binCount=61returns 61 populated bins for orca, raydium, uniswap and pancakeswap (Meteora keeps its own 141), bins straddling the active price correctly; PancakeSwap token amounts match direct on-chainbalanceOf.🤖 Generated with Claude Code
https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj