Skip to content

fix(clmm): retry-ownership gateway side, unified poll status contract, binCount on pool-info - #679

Open
fengtality wants to merge 10 commits into
developmentfrom
feat/lp-close-retry-ownership
Open

fix(clmm): retry-ownership gateway side, unified poll status contract, binCount on pool-info#679
fengtality wants to merge 10 commits into
developmentfrom
feat/lp-close-retry-ownership

Conversation

@fengtality

@fengtality fengtality commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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)

  • Error parser: map Orca Whirlpool 6018 TokenMinSubceededSLIPPAGE_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 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's MATH_OVERFLOW misreport.
  • Pre-broadcast simulation guard: both send paths reject a transaction whose compute-estimation simulation returned an error — stale-state failures become a typed 400 before broadcast (zero fees) instead of an on-chain failure.
  • Orca getPositionInfo contract (ported onto the feat(orca): migrate connector to current Whirlpools SDK #676 SDK migration): returns null only when fetchMaybePosition reports the account does not exist; transient errors propagate. Callers treat null as "position closed", so a swallowed RPC blip could abandon a live funded position while reporting success.
  • Deliberately not included: an in-route close retry loop. Gateway stays a stateless transaction oracle (one request = one attempt, typed errors); retry ownership lives upstream. This is load-bearing: the migrated close route quotes at the config 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 /poll routes spoke different dialects, and both had defects that made a poller unable to act on the answer:

  • Solana returned 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 new NOT_FOUND (-2), which is terminal once the transaction's blockhash expires.
  • Ethereum reported reverted transactions as CONFIRMEDtypeof receipt.status === 'number' ? 1 : -1, and a revert's status is 0, which is a number. It also emitted 2/3 gas-price heuristics no consumer understood, and blocked the request for three 1-second retries before reporting not-found as -1.
  • Both now share TransactionStatusCode: NOT_FOUND (-2) / FAILED (-1) / PENDING (0) / CONFIRMED (1). Transient poll errors report PENDING — an unknown outcome is a reason to poll again, not to give up.
  • Poll error attribution: /poll parsed only JSON.stringify(meta.err), which carries the error code but names no program, so every program-specific code fell through to UNKNOWN. It now parses the err together with meta.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: binCount

GET /trading/clmm/pool-info — the unified route hummingbot-api and condor read through — 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 supported.

  • Unified route accepts binCount and forwards it; Meteora is called without it, as it always returns its own bins.
  • PancakeSwap CLMM gains bin support (it had none). 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 math.
  • Fixes PancakeSwap pool-info token amounts, which were pool.liquidity (V3 virtual liquidity in sqrt-price space) scaled by each token's decimals — the same meaningless figure reported for both sides. Now ERC20 balanceOf on the pool contract, the fix Uniswap already carried.

Dead default RPCs

eth.llamarpc.com (mainnet) and binance.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 now eth-mainnet.g.alchemy.com/public (chainId 0x1) and bsc-dataseed.bnbchain.org (chainId 0x38).

Companion PRs

Validation

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 to FAILED), the binCount passthrough, and PancakeSwap pool-info.

Validated live on mainnet with the stack deployed from these branches:

  • Poll contract: real confirmed / failed / dropped / malformed signatures on Solana, and real confirmed / reverted / unknown transactions on Ethereum, each returning the intended code.
  • Forced close-failure cascade (fault-injected minimums): 11 attempts, one gateway request per attempt, every one rejected pre-broadcast at zero fee cost.
  • binCount=61 returns 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-chain balanceOf.
  • Funded open/close/swap cycles pass on the deployed image.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj

…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-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown

Greptile Summary

The PR standardizes Ethereum and Solana transaction polling, adds typed Solana simulation failures, and extends unified CLMM pool information with configurable bins.

  • Introduces the shared NOT_FOUND, FAILED, PENDING, and CONFIRMED transaction-status contract.
  • Improves Solana error attribution, pre-broadcast simulation handling, and Orca position lookup semantics.
  • Adds shared V3 bin computation, PancakeSwap bin and reserve support, and unified binCount forwarding.
  • Replaces unavailable default Ethereum and BSC RPC endpoints.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the eligible follow-up-review scope.

No blocking failure remains.

Important Files Changed

Filename Overview
src/chains/ethereum/routes/poll.ts Replaces heuristic and delayed transaction statuses with the shared four-state polling contract and correctly classifies reverted receipts.
src/chains/solana/routes/poll.ts Distinguishes unseen signatures from pending transactions, includes program logs in error parsing, and preserves pending status for transient polling errors.
src/chains/solana/solana.ts Adds shared transaction-status handling and rejects structured simulation failures before broadcasting.
src/chains/solana/solana-error-parser.ts Attributes custom errors to the failing program and maps Orca error 6018 to slippage exceeded.
src/connectors/orca/orca.ts Returns null only for definitively absent position accounts while allowing transient lookup failures to propagate.
src/connectors/clmm-v3-utils.ts Centralizes bigint-based V3 tick traversal and bin amount calculations for multiple SDK numeric representations.
src/connectors/pancakeswap/clmm-routes/poolInfo.ts Adds configurable V3 bins and reports pool token reserves using ERC20 balances.
src/trading/clmm/pools.ts Adds bounded binCount input and forwards it to connectors that support configurable bin distributions.
src/schemas/chain-schema.ts Defines the common transaction-status enum used by both chain polling routes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Client[Client] --> Poll[Unified transaction poll]
    Poll --> ETH[Ethereum receipt and mempool lookup]
    Poll --> SOL[Solana transaction and signature-status lookup]
    ETH --> Status[NOT_FOUND / FAILED / PENDING / CONFIRMED]
    SOL --> Status
    PoolClient[CLMM pool-info client] --> Unified[Unified pool-info route]
    Unified --> Connector[Chain-specific connector]
    Connector --> Bins[Optional V3 bin distribution]
Loading

Reviews (10): Last reviewed commit: "docs: cover the poll and CLMM pool-info ..." | Re-trigger Greptile

fengtality and others added 8 commits August 13, 2026 08:01
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>
@fengtality fengtality changed the title fix(solana): typed close errors + pre-broadcast simulation guard; Orca position-info contract (retry architecture) fix(clmm): retry-ownership gateway side, unified poll status contract, binCount on pool-info Aug 13, 2026
@fengtality
fengtality requested a review from rapcmia August 13, 2026 18:01
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>
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