Skip to content

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

Open
fengtality wants to merge 12 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 12 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 moves retry ownership upstream while making Gateway return typed, actionable transaction outcomes and reject known-failing Solana transactions before broadcast.

  • Unifies Ethereum and Solana polling under NOT_FOUND, FAILED, PENDING, and CONFIRMED status codes.
  • Improves Solana program-error attribution and Orca position lookup semantics.
  • Adds binCount forwarding, shared V3 bin calculations, and PancakeSwap pool balances.
  • Replaces nonfunctional 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/solana/solana.ts Adds shared transaction status handling, failed-simulation rejection, and typed landed-transaction error processing.
src/chains/solana/routes/poll.ts Distinguishes unknown signatures from pending transactions and parses transaction errors with program logs.
src/chains/ethereum/routes/poll.ts Replaces heuristic statuses with the unified contract and correctly marks reverted receipts as failed.
src/chains/solana/solana-error-parser.ts Attributes custom errors to the failing program and maps Orca error 6018 to slippage exceeded.
src/connectors/clmm-v3-utils.ts Introduces SDK-neutral bigint calculations for V3 liquidity-bin distributions.
src/connectors/pancakeswap/clmm-routes/poolInfo.ts Adds requested bin distributions and reports actual pool-token balances.
src/trading/clmm/pools.ts Threads validated binCount requests through supported CLMM connectors while preserving Meteora behavior.
src/connectors/orca/orca.ts Restricts null position results to definitive account absence and propagates transient lookup failures.
src/schemas/chain-schema.ts Defines the common cross-chain transaction status contract.
src/schemas/clmm-schema.ts Extends unified pool-info requests and responses with bounded bin-distribution support.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    U[Upstream retry owner] -->|one attempt| G[Gateway route]
    G --> S[Fetch and simulate]
    S -->|simulation error| E[Typed error without broadcast]
    S -->|simulation succeeds| B[Broadcast and confirm]
    B --> P[Unified poll contract]
    P --> C{TransactionStatusCode}
    C --> N[NOT_FOUND]
    C --> F[FAILED]
    C --> W[PENDING]
    C --> O[CONFIRMED]
    I[Unified CLMM pool-info] --> Q[binCount]
    Q --> V[Connector-specific pool reader]
    V --> D[Bin distribution and balances]
Loading

Reviews (12): Last reviewed commit: "refactor(clmm): drop the never-populated..." | 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>
fengtality added a commit to hummingbot/hummingbot-api that referenced this pull request Aug 17, 2026
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
fengtality and others added 2 commits August 18, 2026 16:07
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
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