Skip to content

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

Closed
fengtality wants to merge 31 commits into
developmentfrom
feat/lp-close-retry-ownership
Closed

fix(clmm): retry-ownership gateway side, unified poll status contract, binCount on pool-info#679
fengtality wants to merge 31 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 transaction polling and typed Solana failures, rejects failed simulations before broadcast, extends unified CLMM pool information with configurable bins, and updates connector transaction interfaces.

  • Introduces the shared NOT_FOUND / FAILED / PENDING / CONFIRMED transaction-status contract.
  • Adds CLMM bin distribution support and corrects PancakeSwap pool token balances.
  • Propagates transient Orca position-read failures rather than treating them as closed positions.
  • Updates default Ethereum and BSC RPC endpoints and regenerates the OpenAPI specification.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains within the scope of the available prior review threads.

No blocking failure remains.

Important Files Changed

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]
Loading

Reviews (20): Last reviewed commit: "test(jupiter): assert removed fields' ab..." | 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 12 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
…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
@rapcmia rapcmia moved this to Backlog in Pull Request Board Aug 19, 2026
@rapcmia rapcmia self-assigned this Aug 19, 2026
fengtality and others added 6 commits August 18, 2026 22:30
…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
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
@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown

Too many files changed for review (162 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

fengtality and others added 2 commits August 18, 2026 23:16
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
@rapcmia rapcmia moved this from Backlog to Under Review in Pull Request Board Aug 21, 2026
@fengtality fengtality closed this Aug 21, 2026
@fengtality

Copy link
Copy Markdown
Contributor Author

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.

fengtality added a commit to hummingbot/hummingbot-api that referenced this pull request Aug 21, 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
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.

2 participants