From 338c84402c3197c52fc6f4e54ccff45c79c3411b Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Wed, 12 Aug 2026 18:20:35 -0700 Subject: [PATCH 01/31] fix(solana): typed close-failure errors + pre-broadcast simulation guard; orca position-info contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01HahKfEY9rvKnZijrzUAFSq --- docs/retry-architecture.md | 378 ++++++++++++++++++ src/chains/solana/solana-error-parser.ts | 21 +- src/chains/solana/solana.ts | 54 ++- src/connectors/orca/orca.ts | 16 +- ...sendAndConfirmTransactionForWallet.test.ts | 33 ++ .../chains/solana/solana-error-parser.test.ts | 31 ++ test/connectors/orca/orca.test.ts | 25 +- 7 files changed, 537 insertions(+), 21 deletions(-) create mode 100644 docs/retry-architecture.md diff --git a/docs/retry-architecture.md b/docs/retry-architecture.md new file mode 100644 index 0000000000..5d39360183 --- /dev/null +++ b/docs/retry-architecture.md @@ -0,0 +1,378 @@ +# Retry Architecture: Gateway ↔ Hummingbot + +**Context:** [gateway#678](https://github.com/hummingbot/gateway/issues/678) — an Orca LP close failed with Whirlpool error `6018` (`TokenMinSubceeded`, misreported as `MATH_OVERFLOW`), the LP executor went terminally `FAILED` with the position still open on-chain, and subsequent stop requests returned 404. A proposed fix ([`040e99e`](https://github.com/hummingbot/gateway/commit/040e99ee705e7804066d9912d73751ba13a2a61a)) adds a 3-attempt rebuild-and-retry loop **inside** the gateway's Orca close route. This document formulates the canonical model for where retries live, evaluates that fix against it, and proposes changes. + +--- + +## 1. The principle + +> **Gateway is a stateless transaction oracle.** One HTTP request = one attempt at one on-chain action, built from fresh on-chain state, answered with either a confirmed result or a **typed error**. Hummingbot holds the position/order state, so Hummingbot owns retries — at three distinct altitudes, each answering a different question. + +| Layer | Question it answers | Retry it owns | +|---|---|---| +| **Gateway** | "Did *this one attempt* land?" | Reads & transport only: RPC 429s, confirmation polling until blockhash expiry, tx-data re-fetch. Never re-submits a write. | +| **HB connector** (`gateway.py`, `gateway_base.py`) | "Should I ask Gateway for *another attempt*?" | `_execute_with_retry`: re-POSTs the operation (Gateway rebuilds fresh each time), max 10, only on `TRANSACTION_TIMEOUT` / `status: PENDING`. | +| **LP executor** (`lp_executor.py`) | "What does this failure mean for *the position lifecycle*?" | State-machine re-entry: `control_task` re-invokes `_create_position`/`_close_position` while state stays `OPENING`/`CLOSING`. Reconciles already-succeeded outcomes. | +| **Controller** (`lp_rebalancer.py`) | "What does this failure mean for *the strategy*?" | Re-creates a fresh executor after a `FAILED` one (unbounded, 1 s tick). **Bot topology only** — absent on the `manage_executors` path (§5). | + +This principle is already written into the code on both sides: + +- Gateway `raydium/clmm-routes/removeLiquidity.ts:114`: `// Transaction pending, return for Hummingbot to handle retry` → returns `status: 0` + signature. +- Gateway `solana.ts:1401-1408`: confirmation timeout throws a 504 **including the signature** so "the caller can reconcile." +- Hummingbot `lp_executor.py:594-598` docstring: "Retry logic is handled by the connector." + +```mermaid +flowchart TB + subgraph HB["Hummingbot (stateful — owns retries)"] + C["Controller: lp_rebalancer
strategy-level: re-create executor after FAILED
∞ retries, 1s tick"] + E["LP Executor: lp_executor
lifecycle: state machine re-entry + reconciliation
OPENING/CLOSING re-attempts via control_task"] + K["Connector: gateway.py / gateway_base.py
operation-level: _execute_with_retry
10 re-POSTs, only on TRANSACTION_TIMEOUT / PENDING"] + end + subgraph GW["Gateway (stateless — one attempt per request)"] + R["Route: build tx from FRESH on-chain state
quote → simulate → sign → broadcast"] + S["Chain layer: solana.ts
transport retries only
429 interceptor · confirm-poll to blockhash expiry · tx-data refetch"] + end + C -->|CreateExecutorAction| E + E -->|"_clmm_close_position(max_retries=10)"| K + K -->|"POST /clmm/close-position
(each retry = fresh rebuild)"| R + R --> S + S -->|"CONFIRMED (status 1) · PENDING (status 0 + sig)
or typed error [code: X]"| K + K -->|result or re-raised exception| E + E -->|close_type on terminal| C +``` + +The key property this preserves: **a connector-level retry already gets fresh state for free**, because every POST re-runs the route's fetch → quote → build → simulate pipeline. Gateway-side write retries duplicate that loop one layer down, per-connector. + +--- + +## 2. Retry ownership matrix (as implemented today) + +What actually protects each operation, per layer. ✅ = bounded retry exists, ♻️ = unbounded, ❌ = none (failure is terminal at that layer). + +| Operation | Gateway (writes) | Connector | Executor | Controller | +|---|---|---|---|---| +| **Swap** | ❌ single-shot | ✅ 10× (`_create_order`, `gateway.py:331`) — timeout only | ✅ 10× (`OrderExecutor:334-342`, any failure event) | ♻️ re-create (`lp_rebalancer.py:487`) | +| **Open position / add liq (CLMM)** | ❌ single-shot | ✅ 10× (`_clmm_add_liquidity`, `gateway.py:905`) | ❌ `_handle_create_failure:415` → `FAILED` (deliberate: a blind retry could mint a 2nd position) — but flips to `CLOSING` to recover funds if the add landed | ♻️ re-create same side (`lp_rebalancer.py:543-563`) | +| **Close position (CLMM)** | ❌ single-shot *(the proposed fix makes Orca 3×)* | ✅ 10× (`_clmm_close_position`, `gateway.py:1064`) — **timeout only** | ❌ **`_handle_close_failure:593-602` → `FAILED`, position still live on-chain** | ❌ **nothing — opens a *new* position instead** | +| AMM add/remove, partial remove | ❌ | ❌ direct await, no retry | ❌ | ❌ | +| Pending-tx polling | confirm-poll to blockhash expiry | ♻️ `update_order_status` 1 s forever | — | — | +| Reads (position/pool info) | 429 interceptor | 5 s TTL cache; **exceptions swallowed → `None`** (`gateway.py:1226-1235`) | string-matched reconciliation | — | + +Two structural observations fall out of this table: + +1. **Swaps are retried at three layers; closes at one.** The close's only retry (connector, 10×) fires solely on `TRANSACTION_TIMEOUT`. Every other error — including the stale-minimums slippage error at the heart of #678 — is classified `FAIL_IMMEDIATE` (`gateway_base.py:42-52`, `NON_RETRYABLE_ERROR_CODES` = `SIMULATION_FAILED`, `INSUFFICIENT_BALANCE`, `SLIPPAGE_EXCEEDED`, `INVALID_PARAMS`, `NO_ROUTE_FOUND`). +2. **The one operation that is *safest* to retry is the one with the least retry coverage.** See §3. + +--- + +## 3. Why close differs from swap and open: idempotency + +The proposed fix's justification for retrying *only* Orca close inside gateway is real, but it answers "where is retry **safe**", not "where should retry **live**". + +| Operation | If the outcome is uncertain and you blindly re-submit… | Blind-retry safe? | +|---|---|---| +| Swap | Second swap also executes → **double spend** | ❌ must verify the signature first | +| Open position | Second position minted (fresh mint each attempt) → duplicate exposure | ❌ | +| Add liquidity | Deposits twice | ❌ | +| Partial remove | Removes twice | ❌ | +| **Close position** | Success **consumes the position account**; a second close fails cleanly with "position not found" | ✅ naturally idempotent | +| Collect fees | Second collect transfers ~0 | ✅ mostly | + +So a bounded close-until-closed loop is *correct*. The question is only which layer runs it — and the layered answer is strictly better, because: + +- **Fresh state is free at the connector layer too.** Each re-POST rebuilds quote, minimums, and blockhash. The only thing gateway-side retry buys is a few hundred ms less latency between attempts. +- **The reconciliation already exists upstream.** `lp_executor._close_position:454-486` pre-flights `get_position_info`, and maps "already closed" / "not found" → `_emit_already_closed_event()` → `COMPLETE`. The gateway fix re-implements this same reconciliation inside one route, for one connector. +- **Per-connector divergence.** There are six close-position routes (orca, meteora, raydium, pancakeswap, pancakeswap-sol, uniswap). A gateway-side loop fixes one; a hummingbot-side policy fixes all of them at once, plus every future connector. +- **Retry stacking.** The connector still wraps the route in a 10× loop. With the gateway loop inside, a timeout-flavored failure can trigger up to 10 × 3 = 30 build/submit cycles per close, invisible to the strategy. Neither layer sleeps between attempts. +- **Policy visibility.** Retry counts, backoff, and give-up conditions are strategy decisions. Buried in a route they can't be configured per-executor, surfaced in `get_custom_info`, or reasoned about from the bot. + +--- + +## 4. Anatomy of issue #678 + +```mermaid +sequenceDiagram + participant CT as Controller + participant EX as LP Executor + participant CN as Connector + participant GW as Gateway (Orca route) + participant SOL as Solana + + EX->>EX: state = CLOSING + EX->>CN: _clmm_close_position (max_retries=10) + CN->>GW: POST /clmm/close-position (attempt 1) + GW->>SOL: fetch position + pool, build tx ONCE
(minimums from now-stale quote) + GW->>SOL: simulate (compute units) + Note over GW: sim err ignored — only unitsConsumed read + GW->>SOL: broadcast + SOL-->>GW: landed on-chain but FAILED: custom error 0x1782 (6018) + GW-->>CN: 400 [code: SIMULATION_FAILED]
"MATH_OVERFLOW" (6018 missing from Orca map) + Note over CN: SIMULATION_FAILED ∈ NON_RETRYABLE
→ FAIL_IMMEDIATE (gateway_base.py:782) + CN-->>EX: re-raise + EX->>EX: _handle_close_failure → state = FAILED + EX->>EX: next tick: close_type = FAILED, stop() + Note over EX: position STILL OPEN on-chain + CT->>CT: sees FAILED → creates NEW executor,
opens a SECOND position (lp_rebalancer.py:543) + Note over CT,EX: stop request for old executor → 404
(popped from API memory on completion — see §5) +``` + +Note the punchline: **the proposed error-mapping fix alone doesn't change the outcome.** Mapping 6018 → `SLIPPAGE_EXCEEDED` produces a clearer message, but `SLIPPAGE_EXCEEDED` is *also* in `NON_RETRYABLE_ERROR_CODES` — correct for a swap (retrying the same quote re-fails), wrong for a close (the fix *is* rebuilding, which a re-POST does). This is why the retry decision must be **operation-aware**, and why it lives naturally in the connector, which knows which operation it is running. + +--- + +## 5. The second topology: `manage_executors` has no controller + +The stack in §1 describes a **deployed bot** running the `lp_rebalancer` controller. But users (and condor agents) also open and close LP executors directly through the `manage_executors` MCP tool, and on that path the topology is different: **the executor runs inside the hummingbot-api process, and the controller layer does not exist.** + +```mermaid +flowchart TB + subgraph TA["Topology A — deployed bot"] + A0["lp_rebalancer controller
♻️ re-creates after FAILED"] --> A1["ExecutorOrchestrator"] --> A2["LPExecutor"] + end + subgraph TB2["Topology B — manage_executors (condor / API)"] + B0["condor agent tick / Telegram user
⚠️ prompt lists RUNNING executors only"] --> B1["manage_executors MCP tool
condor mcp_servers/hummingbot_api"] --> B2["hummingbot-api ExecutorService
in-process dict _active_executors
❌ FAILED handled same as COMPLETE"] --> B3["LPExecutor
(same class, runs in the API process)"] + end + A2 --> GW["Gateway"] + B3 --> GW +``` + +Facts, from tracing condor + hummingbot-api: + +- **`controller_id` is a label, not an owner.** `ExecutorService.create_executor` (`hummingbot-api/services/executor_service.py:424-488`) instantiates `LPExecutor` directly against a trading-interface shim and stores `controller_id` (default `"main"`, `:455`; condor agents pass their agent id, `condor/agents/prompts.py:350`) purely for filtering. No `ExecutorOrchestrator`, no `ControllerBase` is ever involved. +- **Terminal means forgotten.** The 1 s `_control_loop` (`executor_service.py:293-313`) detects `is_closed` and calls `_handle_executor_completion` (`:652-680`), which **pops the executor from memory** (`:657`), persists the final state, and returns. `CloseType.FAILED` takes the same path as `COMPLETE` — no re-creation, no alert, no orphan flag. +- **The #678 stop-404, precisely.** `stop_executor` looks up the in-memory dict and 404s on a miss (`executor_service.py:630-632`). Because the completion handler pops within one tick of the executor closing, the "already closed → 400" branch (`:634-635`) is effectively dead code — a stop against any terminal executor returns **404**. Meanwhile `GET /executors/{id}` falls back to the DB, so the agent sees an executor that is *findable but un-stoppable*. A second orphan source needs no gateway error at all: an API restart rewrites every RUNNING row to `TERMINATED/SYSTEM_CLEANUP` (`executor_service.py:229-259`) while the on-chain position lives on. +- **The condor agent cannot see the failure.** The executor summary injected into the agent's tick prompt lists **only `status == "RUNNING"`** executors (`condor/condor/agents/providers/executors.py:55-67`). A `FAILED` LP executor doesn't raise a flag — it *vanishes from view*, with a live position still on-chain. Nothing in condor branches on `close_type`; recovery exists only as prose in `guides/lp_executor.md:174-183` ("close the position via the DEX UI… manually update the executor status in the database"). + +Consequence for the retry model: in Topology B the ownership stack **stops at the executor**. The executor layer is the only layer present in both topologies — which makes the executor-level close retry (E1 below) the keystone fix, and means `lp_rebalancer`'s recovery duties (re-create / reconcile orphans) need a topology-B home: the `ExecutorService` completion handler and the condor data provider, not a controller. + +--- + +## 6. Verdict on commit `040e99e` + +| Piece of the fix | Verdict | Why | +|---|---|---| +| Map Orca `6018` → `SLIPPAGE_EXCEEDED` (`solana-error-parser.ts`) | ✅ **merge** | Pure correctness. Typed errors are Gateway's core job. | +| Reject simulation `err` before broadcast (`solana.ts`, both send paths) | ✅ **merge** | Fail-fast with a typed 400 instead of broadcasting a doomed tx and paying fees. Stateless. Benefits every connector. | +| 3-attempt rebuild-and-retry loop in `closePosition.ts` | ❌ **don't merge — relocate to Hummingbot** | Write-retry policy inside a stateless service; Orca-only; stacks under the connector's 10×; duplicates executor reconciliation. | +| In-route reconciliation (position gone → check attempted signature → return success) | ❌ **don't merge** | Same capability already exists at `lp_executor.py:454-486` (pre-flight) and can be reached on the next CLOSING re-entry. Keeping one reconciliation point avoids two layers disagreeing. | + +--- + +## 7. Proposed changes + +### The state machine, before and after + +```mermaid +stateDiagram-v2 + direction LR + state "Current" as cur { + [*] --> CLOSING + CLOSING --> COMPLETE: closed / already closed + CLOSING --> FAILED: ANY close error
(after connector 10x timeout-only) + FAILED --> [*]: stop() — position still live + } + state "Proposed" as prop { + [*] --> CLOSING2 + CLOSING2: CLOSING + CLOSING2 --> COMPLETE2: closed / already closed
(pre-flight reconciles landed tx) + CLOSING2 --> CLOSING2: close error and retries left —
stay CLOSING, control_task re-enters
(fresh rebuild via re-POST) + CLOSING2 --> FAILED2: retries exhausted + COMPLETE2: COMPLETE + FAILED2: FAILED — position flagged as orphan,
controller must not blind-reopen + } +``` + +> **Terminal state superseded by §10:** exhaustion with the position still on-chain now terminates as an **involuntary `POSITION_HOLD`** (`hold_reason: "close_retries_exhausted"` + a zero-amount marker order carrying the position address) rather than `FAILED`; `FAILED` is reserved for exhaustion with nothing left on-chain. The `CLOSING → CLOSING` retry loop above is unchanged. + +### Hummingbot — the real fix + +**E1 (core). Bounded close re-entry in the executor.** `_handle_close_failure` (`lp_executor.py:593-602`): instead of jumping to `FAILED`, increment `_current_retries` and *return with state still `CLOSING`*. `control_task` (`lp_executor.py:163-167`) already re-invokes `_close_position` every tick for exactly this purpose — the machinery exists and is currently unreachable (`data_types.py:159-166` deliberately preserves `CLOSING` across updates). Bound it against `_max_retries` (implemented inline in the handler rather than via the inert `evaluate_max_retries` — same outcome, one owner); go `FAILED` only on exhaustion, with exponential backoff (capped 30 s) between attempts so instant transport failures (e.g. a Gateway restart) can't burn the budget in seconds. The pre-flight at `_close_position:454-486` gives every re-entry the timeout-but-landed reconciliation for free. Since the executor owns the retry policy, the connector's inner budget for close is cut to 3 — leaving it at 10 would stack into ~O(n²) submissions per close, the same stacking §3 rejects. This is the same loop the gateway fix implements, one layer up, for **all** connectors. + +**H1. Operation-aware retry classification in the connector.** `_execute_with_retry` (`gateway_base.py:674-753`) treats `SLIPPAGE_EXCEEDED`/`SIMULATION_FAILED` as fail-immediate globally. Add a `retryable_error_codes` parameter; `_clmm_close_position` opts in to `{SLIPPAGE_EXCEEDED, SIMULATION_FAILED, TX_NOT_CONFIRMED}` because each re-POST rebuilds from fresh state and close is idempotent (§3). `TX_NOT_CONFIRMED` is the newly-typed code on the landed-but-failed branch (`status: -1`), which previously raised with no `[code:]` at all — meaning the exact #678 failure shape (tx broadcast, failed on-chain) was unretryable even with the opt-in. Swaps and opens keep today's conservative set. (E1 alone would also cover this at backoff granularity; H1 makes the fast path fast.) + +**H3 (latent bug, load-bearing).** `get_position_info` (`gateway.py:1226-1235`) swallows every exception → `None`, and the executor reads `None` as "position already closed" → `COMPLETE` (`lp_executor.py:462-468`). A transient RPC error during close can therefore **abandon a live position while reporting success** — worse than #678. Three coupled requirements, each necessary: +- `None` only for a **position-specific** 404 ("Position not found…"/"Position closed…"); a bare "not found" match also catches the HTTP client's "(Not Found)" suffix on unrelated 404s (missing route after a redeploy, missing wallet file) and would report a live position as gone. Everything else re-raises. +- Existence decisions must use an **uncached** read (`get_position_info_fresh`): the 5 s `@async_ttl_cache` stores `None` like any value, so one cached 404 would be served for several consecutive 1 s ticks and masquerade as independent confirmations. +- External-close detection additionally requires the position to have been **seen on-chain at least once** plus 3 consecutive fresh misses — a not-found streak on a just-created position is RPC lag, not a close. + +**C1. Controller must not blind-reopen over a live position.** `lp_rebalancer.py:543-563` re-creates after `FAILED` without checking `custom_info["position_address"]`. If the failed executor still holds a live position address: halt-and-log instead of opening a second position. The `position_hold` accounting skip at `514-518` ("no tokens deposited" — false for close failures) is deliberately retained: with the halt in place, no replacement executor is created, so there is no round trip to book; booking one would require amounts the failed close never produced. Known limitation: the halt is in-memory only — a bot restart clears it and the controller will blind-reopen; the durable guard for that case is the orphan flag + listing at the API layer (A1/A4). + +**H2 (hygiene).** Add small sleep/backoff between `_execute_with_retry` attempts (today pacing relies entirely on the blocking POST), and expose `current_retries`/`max_retries` in `LPExecutor.get_custom_info` the way `OrderExecutor` does (`order_executor.py:353-354`). + +### hummingbot-api & condor — the controller's duties, without a controller (Topology B) + +**A1. Flag orphans at the one place every executor passes through.** `_handle_executor_completion` (`hummingbot-api/services/executor_service.py:652-680`): when `close_type == FAILED` and `custom_info` still carries a `position_address`, persist an **orphaned-position flag** on the executor record (and log at error level) instead of treating it like any other completion. Expose orphans via `/executors/search` and the positions summary so both the dashboard and agents can query them. This is `lp_rebalancer`'s "skip accounting / react to FAILED" role, relocated to the layer that actually owns Topology-B executors. + +**A2. Make stop DB-aware instead of 404.** `stop_executor` (`executor_service.py:630-632`) should fall back to the DB the way `get_executor` (`:563-592`) already does: a stop against a terminal executor returns its final state as a no-op success ("already terminated, close_type=FAILED, orphaned position at X…"), and 404 is reserved for ids that never existed. This directly removes the second #678 symptom, and turns a dead end into an actionable answer for the agent. + +**A3. Let the condor agent see failures.** The tick-prompt executor summary (`condor/condor/agents/providers/executors.py:55-67`) filters to RUNNING; include terminal-with-orphan executors (from A1, or minimally any recent `close_type=FAILED` with a `position_address`) in the narrative summary so the agent's next tick can react — retry the close via `manage_executors`/`manage_clmm`, or alert the user. Today the failure is silently invisible. + +**A4. Codify chain-vs-tracked reconciliation.** The guides already instruct agents in prose to cross-check `positions_owned` / `get_portfolio_overview(include_lp_positions=True)` against executor `custom_info.position_address` (`condor guides/lp_executor.md:176-183`). Promote this to code: a hummingbot-api endpoint (or condor routine) that diffs on-chain positions against tracked executors and reports strays — this also catches the API-restart orphan class (`SYSTEM_CLEANUP`), which involves no gateway error at all. + +### Gateway — keep it stateless + +**G1. Merge** the `6018` error mapping from `040e99e`. + +**G2. Merge** the simulation-`err` rejection from `040e99e` (both `sendAndConfirmTransaction` paths). This converts most stale-state failures into a pre-broadcast typed 400 — no fee spent, immediately retryable upstream. + +**G3. Drop** the in-route retry/reconciliation loop from `closePosition.ts`. The route stays: fetch fresh → build → send → report, with `404 Position not found` continuing to serve as the upstream "already closed" signal. + +### Rollout order + +1. **G1 + G2** — small, standalone gateway PR (rebased after the Orca SDK branch merges). +2. **E1 + H1 + H3** — one hummingbot PR: makes closes retry-until-closed for every CLMM connector **and both topologies**, and fixes the false-COMPLETE hazard. +3. **A1 + A2** — hummingbot-api PR: orphan flagging on completion, DB-aware stop (kills the 404 symptom). +4. **A3 + C1** — condor provider surfaces failures to the agent; `lp_rebalancer` orphan guard for the bot topology. +5. **A4 + H2** — reconciliation endpoint/routine, backoff, retry-counter observability. + +Note the leverage ordering: E1 fixes the stranding at the only layer shared by both topologies; A1–A3 exist so that *when* E1 exhausts its retries, the failure is visible and actionable instead of silently orphaning a funded position. + +--- + +## 8. Implementation status (2026-08-12) + +All of the above is implemented as uncommitted working-tree changes: + +| Change | Repo / files | Notes | +|---|---|---| +| **G1** 6018 → `SLIPPAGE_EXCEEDED` | gateway: `solana-error-parser.ts` (+ test) | Ported from `040e99e` | +| **G2** simulation-`err` rejection | gateway: `solana.ts` (+ test) | Only the compute-estimation guard was still missing on `development` — the wallet chokepoint already pre-flights via `simulateWithErrorHandling`; the fix branch was based on an older base. The retry loop was **not** ported, per §6. | +| **E1** bounded close re-entry | hummingbot: `lp_executor.py` `_handle_close_failure` | Stays `CLOSING`, increments `_current_retries` (wires the dead `_max_retries_reached` field); `FAILED` only on exhaustion | +| **H1** operation-aware retry codes | hummingbot: `gateway_base.py` `_execute_with_retry`/`_classify_error`, `gateway.py` `_clmm_close_position` | Close opts in to `SLIPPAGE_EXCEEDED` + `SIMULATION_FAILED` | +| **H2** backoff + observability | hummingbot: `gateway_base.py` (1 s between attempts), `lp_executor.py` `get_custom_info` (`current_retries`/`max_retries`/`max_retries_reached`) | | +| **H3** `get_position_info` contract | hummingbot: `gateway.py` | `None` = definitive 404 only; transient errors re-raise. `_update_position_info` treats 3 consecutive definitive misses as an external close; post-create fetch guarded so a blip can't route a fresh position to `CLOSING` | +| **C1** orphan guard | hummingbot: `controllers/generic/lp_rebalancer` + mirrored in hummingbot-api `bots/controllers/...` | Halts re-creation when the FAILED executor still reports a `position_address` | +| **A1** orphan flag | hummingbot-api: `executor_service.py` | `orphaned_position: true` injected into persisted `final_state`; error-level log on completion | +| **A2** DB-aware stop | hummingbot-api: `executor_service.py`, `models/executors.py` | Terminal executor → `status="already_terminated"` no-op with `close_type`/`position_address`/`orphaned_position`; 404 only for unknown ids | +| **A4** orphan listing | hummingbot-api: `GET /executors/positions/orphaned` (router + service + repository) | DB-side: FAILED-with-position + `SYSTEM_CLEANUP` LP restarts (flagged `needs_onchain_reconciliation`) | +| **A3** agent visibility | condor: `agents/providers/executors.py`, `guides/lp_executor.md` | Orphaned executors surface in the tick-prompt narrative (`🚨 ORPHANED POSITION`) and in provider data; guide updated for the new stop/orphan contract | + +Validation: gateway — targeted jest (63 passed), `tsc` and eslint clean; hummingbot — `test/hummingbot/connector/gateway/` + lp_executor suites (184 passed, incl. new contract tests), flake8 clean; hummingbot-api — syntax/import checks, pre-existing `test_gateway_lp_executor.py` staleness confirmed unrelated via stash; condor — MCP tools + executor/performance suites pass. + +--- + +## 9. Adversarial review outcomes (2026-08-12) + +Two independent adversarial reviews were run against the implementation. Confirmed findings and their resolutions: + +| Finding | Severity | Resolution | +|---|---|---| +| `@async_ttl_cache` stores `None`, so one cached 404 satisfied the "3 consecutive misses" guard in 3 ticks; combined with a lagging post-create read, a **live fresh position could be falsely COMPLETEd and the controller would stack a second one** | Critical | `get_position_info_fresh` (uncached) added; all existence decisions (close pre-flight, monitoring, post-create) use it. Guard additionally gated on the position having been seen on-chain at least once. Both behaviors pinned by tests | +| "None = definitive 404" was substring-matched on "not found" — the HTTP client stamps "(Not Found)" on **every** 404, so a missing route (Gateway redeploy) or missing wallet file read as "position gone" | Critical | Match narrowed to position-specific messages (verified against actual Gateway route strings); contract pinned by a dedicated test file | +| Retry stacking: executor (11) × connector (11) = up to 121 build/submit cycles, ~1.9 h per close on timeouts — worse than the 10×3 stacking §3 condemned | Major | Connector inner budget for close cut to `min(3, max_retries)`; executor owns the policy | +| Transport errors (connection refused/5xx, no `[code:]`) fail instantly → all executor retries burned in ~11 s during a 20 s Gateway restart → permanent FAILED + halt | Major | Exponential backoff (2ⁿ s capped at 30 s) between executor close attempts; a Gateway restart now spans a few retries instead of all of them | +| Landed-but-failed close (`status: -1`) raised with no error code → unretryable even with the H1 opt-in — the flagship #678 shape missed the fast path | Major | Raise now typed `[code: TX_NOT_CONFIRMED]`; close opts in | +| Agent-facing recovery text said a "fresh lp_executor" could recover an orphan — it cannot (configs have no adoption field); following it would mint a second position | High | Wording corrected everywhere (provider, guide, MCP outputs): gateway-side close by position address, then `resolve_orphan` | +| `GET /executors/positions/orphaned` was unreachable from the MCP tool it was documented for | High | `manage_executors` gained `orphaned` and `resolve_orphan` actions (raw-HTTP passthrough, same pattern as `get_logs`) | +| Orphan listing applied `limit` to mixed candidates then filtered in Python — restarts/failed swaps could push real orphans past the newest 500 and return a false "no orphans" | Med-High | `executor_type='lp_executor'` filter moved into SQL (the only type owning position accounts); DB errors now raise instead of returning `[]` | +| No way to ever silence a recovered orphan — 🚨 warnings forever | Medium | `POST /executors/{id}/resolve-orphan` sets `orphan_resolved` in the final state; listing, provider, and warnings all skip resolved records | +| "404 = never existed" was overclaimed: completion race, failed persist, or DB blip still 404'd a real executor | Medium | Stop now treats any DB-known, not-in-memory executor as `already_terminated` regardless of stored status (reconciling stale RUNNING rows); docs soften the 404 meaning | +| Telegram handler and MCP stop output showed "✅ stopped" for a no-op stop, hiding the orphan payload | Medium | Both presenters branch on `already_terminated` and surface the orphan warning | + +### Live smoke test (2026-08-12) + +The hummingbot wheel was rebuilt (`hummingbot-20260729-cp312`, macOS arm64) and force-reinstalled into the `hummingbot-api` conda env; import inspection confirmed the installed package carries every change (fresh reads, opt-in codes, `TX_NOT_CONFIRMED`, E1 backoff, seen-gate). The patched gateway ran in dev mode on :15889 (the production container on :15888 untouched), and the **H3 contract was verified end-to-end through the installed wheel against the live gateway** — all read-only, no on-chain writes: + +1. Nonexistent position on a live Orca route → connector returns `None` (`"Position not found or closed: …"` 404) ✅ +2. Nonexistent connector route (the HTTP client stamps `(Not Found)` on it) → connector **raises** instead of `None` ✅ +3. Dead gateway port → connector raises `ClientConnectorError` ✅ + +The smoke test also caught two live gateway bugs, both fixed and re-verified: + +- **Close-position on a missing position returned a generic 500**, not the 404 callers reconcile on: the legacy whirlpool SDK's `getPosition` *throws* (`"Unable to fetch Position at address…"`) instead of returning null, so the route's not-found branch was dead code. The close route now maps that throw to `404 Position not found`. +- **`Orca.getPositionInfo` swallowed *every* error into `null`** — meaning a transient RPC failure inside Gateway was served as the definitive `"Position not found or closed"` 404, silently reopening the abandon-live-position hole one layer *below* the H3 connector fix. It now returns null only for the SDK's definitive account-missing throw and rethrows everything else (transient errors surface as 500s with no position-specific text → the connector raises). Both behaviors pinned by tests; 216 gateway tests pass. + +### Docker rebuild + funded live test (2026-08-12) + +Both production containers were rebuilt with the patched code and the full cycle was run with real funds on Orca mainnet: + +- **Images**: a linux-arm64 wheel (`hummingbot-20260729-cp312-linux_aarch64`) was built in a container from the patched source and layered onto `hummingbot/hummingbot-api:latest` together with the updated API source (`Dockerfile.patched-hummingbot`); `hummingbot/gateway:development` was rebuilt from the patched checkout. Both containers were recreated with identical wiring (conf/cert volumes, `emqx-bridge` network, 127.0.0.1 port binds); in-container inspection confirmed the patched code, and the new routes appeared in the live OpenAPI spec. +- **Funded cycle** (wallet `82Sgg…qyHx5`, Topology B via `POST /executors/`): opened a RANGE lp_executor on the Orca SOL-USDC 0.04% whirlpool (`Czfq…44zE`) with 0.04 SOL + 3 USDC → position `6hep…Hkup` live on-chain, `IN_RANGE`, amounts verified independently through the gateway. Stopped with `keep_position=true` → `CLOSING` → closed on-chain first-attempt (`current_retries: 0/10` visible in `custom_info` — H2 observability live), `POSITION_HOLD/COMPLETE`, full rent refunded (0.01006 SOL), fees earned recorded, total tx cost ~0.00004 SOL. +- **Post-close verification**: gateway position-info returns the position-specific 404 for the closed position; a second stop returned `status: "already_terminated"` with final `close_type` — **the exact #678 dead-end (stop → 404) is gone in production**; `/executors/positions/orphaned` correctly empty. +- **Bonus real-world failure exercised**: the first create attempt failed on a public-RPC rate limit — the executor went `FAILED` with **no** position address, was correctly **not** flagged as an orphan (nothing landed on-chain, no funds spent), and the fix was a conf change (QuickNode RPC) + retry. Exactly the intended failure semantics for the open path. + +### Close-failure stress test (2026-08-12) + +**Natural stress: 5× narrow open→close cycles.** Five positions opened at ±0.07% around the live price on the SOL-USDC 0.04% whirlpool and closed immediately — all five closed first-attempt (`close_retries: 0`), full rent refunded, zero orphans. The natural 6018 race is effectively unreachable on this branch because the legacy-SDK close route quotes withdrawal minimums with a **50% slippage buffer** (the fix branch's SDK rework quotes at the configured `slippagePct` — which is what made #678 possible at ~1%). + +**Forced failure: fault-injected gateway image.** To exercise the full failure machinery deterministically, a throwaway `gateway:close-stress-test` image set the close instruction's token minimums to 2×actual+1 — guaranteeing Whirlpool `6018 TokenMinSubceeded` on every attempt. One funded position (`9xiY…D4tW`) was opened and stopped against it. Observed, end to end on production containers: + +1. **G2 made every failed attempt free**: each of the 33 gateway POSTs (11 executor attempts × 3 connector retries) was rejected by the compute-simulation guard *before broadcast* — zero fees burned across the entire cascade. +2. **H1 opt-in retried at the connector**: 3 inner attempts per executor attempt, ~1.8 s apart. +3. **E1 bounded re-entry with visible backoff**: `close_retries` climbed 0→11 over ~5 minutes with the exponential backoff plainly in the timestamps (2, 4, 8, 16, then 30 s capped), each re-entry rebuilding from fresh state. +4. **Terminal + A1**: exhaustion → `FAILED`, `max_retries_reached: true`, `orphaned_position: true` in the persisted state, error-level orphan log pointing at the recovery endpoint. +5. **A4/A2 live**: `/executors/positions/orphaned` listed the record with the position address; re-stop returned `already_terminated` + `orphaned_position: true` + the address — the actionable payload instead of a 404. +6. **Recovery**: gateway restored to the normal image → direct close succeeded first try (all funds + rent recovered) → `resolve-orphan` cleared the listing and the flag. + +**The forced failure also caught a real parser bug** — the same misreporting #678 complained about, in a shape unit tests missed: `extractProgramId` matched the *first* `Program X invoke|failed` in the message, and simulation logs open with prelude programs (ComputeBudget), so the failing DEX program's error table was never consulted and Orca 6018 fell through to the generic map's `MATH_OVERFLOW` / `SIMULATION_FAILED`. The close survived anyway **only because** the opt-in set includes both `SIMULATION_FAILED` and `SLIPPAGE_EXCEEDED` — defense-in-depth proving its worth live. Fixed: the parser now attributes a custom error to the program on the `failed: custom program error` line (regression test with a full simulation-shaped log; 53 parser tests pass), and the rebuilt `gateway:development` image carries the fix. Also fixed from live observation: an off-by-one that ran `max_retries + 1` close attempts while logging `n/max`. The hummingbot-api image was subsequently rebuilt with the corrected wheel and redeployed; a 2-cycle funded open/close retest on the same pool passed clean (0 retries, rent refunded, 0 orphans), so the deployed stack is fully current with source. + +Accepted residual risks (documented, not fixed): +- **C1 halt is process-local** — a bot restart clears it and the controller can blind-reopen; the durable guard is the API-layer orphan flag/listing, which is Topology-B infrastructure. A bot-side persistent guard would need controller state persistence that doesn't exist today. +- **Force-stop stragglers** — `force_stop_with_position_hold` doesn't cancel an in-flight `_clmm_close_position`; a lingering attempt can land *after* the executor is persisted FAILED-with-position, making the orphan flag stale-wrong. `resolve_orphan` (after an on-chain check) is the correction path. +- **Deployment coupling** — hummingbot-api runs the *installed* hummingbot wheel; E1/H1/H3 reach Topology B only after the wheel is rebuilt and reinstalled (see `~/.claude/CLAUDE.md` build steps). Until then the API-side changes are live but the executor still hard-fails closes. +- **SYSTEM_CLEANUP orphans have no position address** (final state was never persisted) — they are listed as `needs_onchain_reconciliation` rather than resolved automatically. + +--- + +## 10. Redesign: exhausted close ends as an involuntary POSITION_HOLD (2026-08-12) + +§7's terminal state — `FAILED` with the position flagged as an orphan — solved #678 but required a parallel side-channel (the `orphaned_position` flag, the orphan listing, the C1 halt, condor prompt injection) at every layer. Surveying how the rest of the executor family handles failure shows why: **we picked the wrong terminal close type**, and every consumer then needed bespoke plumbing to compensate. This section supersedes the terminal-state design in §7 (E1's *retry loop* is unchanged — it matched the family all along). + +### What the executor family already does + +- **In-executor bounded retry is the standard, not an LP invention.** Order, grid, DCA and TWAP executors all increment `_current_retries` on failure and stay in state; the base class's `control_loop` calls `evaluate_max_retries()` after every `control_task`, and that hook terminates the executor when `_current_retries > _max_retries` (`executor_base.py:254-262`). E1 re-implemented exactly this shape inline. +- **`FAILED` means "abnormal end with *no residual exposure*".** `force_stop_with_position_hold` (`executor_base.py:241-252`) sets `POSITION_HOLD if held_orders else FAILED`: anything that still represents exposure terminates as a hold with `held_position_orders`; `FAILED` is reserved for executors with nothing left behind. +- **The hold channel has a real consumer on both topologies.** The orchestrator folds `held_position_orders` from `POSITION_HOLD` executors into durable `PositionHold` records (`executor_orchestrator.py:585-640`); hummingbot-api's `ExecutorService` has the same machinery independently, **including recovery from the database on restart** (`executor_service.py:159-224`). Ending close-exhaustion as `FAILED` bypassed all of it. + +### The constraint: a live LP position is not spot exposure + +`PositionHold` is a spot net-position accumulator (amounts, avg entry, realized PnL). A still-open on-chain position cannot be booked as spot amounts — the tokens sit in the pool, not the wallet — which is exactly why the forced-stop path logs "an on-chain position cannot be represented as spot orders" (`lp_executor.py:986-1005`) instead of inventing amounts. The LP executor already has the answer for "report something without booking anything": the **zero-amount marker order** (`lp_executor.py:904-921`), which `PositionHold._process_order` skips (`amount_base == 0` returns early) while still making `held_position_orders` non-empty and carrying metadata. + +### The design + +1. **`_handle_close_failure` only counts and paces.** It increments `_current_retries`, arms the exponential backoff, and stays `CLOSING`. The terminal decision moves out of the handler and into the family hook. +2. **`evaluate_max_retries` is overridden in `lp_executor`** (previously inert there). Base semantics (`>`, i.e. `max_retries + 1` total attempts — the family convention; this supersedes the `>=` change from §9's stress test) decide *when*; the override decides *how*: + - **Position still on-chain** → append a zero-amount marker order carrying `position_address`, `lp_unclosed_position: true`, and the tx fees burned so far; set `hold_reason: "close_retries_exhausted"`; terminate as **`POSITION_HOLD`**. Accounting is untouched (zero amounts), but the hold — with the position's identity — lands in the durable hold store on both topologies. + - **No position** (close exhausted after the position turned out to be gone, or a failure path with nothing on-chain) → `FAILED`, now true to its family meaning. +3. **Voluntary vs involuntary holds are distinguished by `hold_reason`**, not by close type. A `keep_position=True` stop and an exhausted close both end `POSITION_HOLD`; only the latter carries `hold_reason` (and the marker order instead of a net-trade order). +4. **Consumers key off the reason, not `FAILED`:** + - `lp_rebalancer`: the halt-and-log guard (C1) now triggers on an involuntary hold (`hold_reason` set) as well as legacy `FAILED`-with-position; the accounting branch must **also skip** involuntary holds — their `base_amount`/`quote_amount` are pool balances of a still-open position, and booking them as returned tokens would corrupt `position_hold` tracking. + - hummingbot-api: `orphaned_position` flag injection, the orphan listing, and `resolve-orphan` accept the `POSITION_HOLD`+`hold_reason` shape (legacy `FAILED`-with-position and `SYSTEM_CLEANUP` remain as defense for force-stop stragglers and restarts). The orphan listing becomes a *view* over involuntary holds rather than a parallel mechanism. + - condor: the provider's orphan filter already keys off the injected flag, so it picks up the new shape automatically; guide wording updates from "FAILED with position" to "involuntarily held". + +### What this closes + +- **The C1 restart residual shrinks**: the involuntary hold is persisted in the hold store (DB-recovered on the API path) rather than living only in a controller field. +- **Family semantics restored**: `FAILED` again implies nothing-left-on-chain; dashboards and PnL reports that special-case `POSITION_HOLD` see the exposure through the same lens as every other executor's residue. +- **One mental model for users**: condor users already understand `POSITION_HOLD` from `keep_position=True` stops; an exhausted close is the same object with a reason attached, not a new 🚨 concept. + +### Implementation status (2026-08-12, uncommitted) + +| Change | Repo / files | +|---|---| +| `_handle_close_failure` counts + paces only; `evaluate_max_retries` override terminates (involuntary `POSITION_HOLD` + marker order with live position, `FAILED` without); `_hold_reason` in `get_custom_info`; family `>` semantics restored | hummingbot: `lp_executor.py` (+ contract tests, 140 pass) | +| Accounting skip + halt guard keyed on `hold_reason` as well as legacy `FAILED`-with-position; closed-bounds capture excludes involuntary holds | hummingbot `controllers/generic/lp_rebalancer` + hapi `bots/controllers/...` mirror | +| Orphan flag injection, listing, and resolve accept the `POSITION_HOLD`+`hold_reason` shape; `hold_reason` in stop/orphan payloads and models; router docs | hummingbot-api: `executor_service.py`, `models/executors.py`, `routers/executors.py` | +| Provider orphan filter adds the `hold_reason` shape; prompt/guide wording updated from "terminated FAILED" to reason-aware | condor: `agents/providers/executors.py`, `guides/lp_executor.md` (146 tests pass) | + +Deliberately unchanged: `force_stop_with_position_hold` still ends a live-position forced stop as `FAILED` (the close may still be in flight at the shutdown deadline; the legacy `FAILED`-with-position orphan class covers it), and the zero-amount marker is not emitted there. The deployed hapi image predates this redesign until the next wheel/image rebuild. + +### Live validation (2026-08-12, rebuilt stack) + +Wheel → image → funded-retest pipeline re-run with the §10 redesign deployed (linux wheel rebuilt in-container, hapi image relayered, container recreated; in-container checks confirmed the involuntary-hold code and `hold_reason` plumbing). + +**Clean cycles (normal gateway image):** two funded open/close cycles on the SOL-USDC whirlpool closed first-attempt (`POSITION_HOLD`/`COMPLETE`, 0 retries, rent refunded, orphan listing empty). A bonus negative test happened naturally: two *opens* were rejected at simulation with Orca `0x177c` (`TokenMaxExceeded` — fast-moving market vs a ±0.07% range) and terminated `FAILED` **with nothing on-chain and no orphan record** — `FAILED`'s restored family meaning, observed live at zero fee cost. + +**Forced cascade (fault-injected image, minimums 2×actual+1):** position `ApT4…Qk8A`, executor `C5y3…sTrG`: +- 11 close attempts (`max_retries + 1`, family `>` semantics) over ~5 min with the 2/4/8/16/30 s backoff visible in the timestamps; every attempt rejected pre-broadcast (zero fees). +- Terminal: `close_type: POSITION_HOLD`, `hold_reason: close_retries_exhausted`, `max_retries_reached: true`, and the zero-amount marker order in `held_position_orders` carrying the position address (`lp_unclosed_position: true`). +- `orphaned_position: true` injected; `/positions/orphaned` listed the `POSITION_HOLD` record with `hold_reason`; re-stop returned `already_terminated` + reason. +- Recovery: normal image restored → direct close first-attempt (0.0174 SOL + 1.448 USDC + fees + 0.0100572 rent recovered) → `resolve-orphan` accepted the `POSITION_HOLD` candidate and cleared the listing. Fault image deleted. + +### Rebase note: Orca SDK migration (gateway#676) + +The Orca connector's migration to the current whirlpools SDK merged into `development` while this work was in flight, which changed the gateway-side surface: + +- The new close route quotes at `slippageToleranceBps` from the connector config (~1%) instead of the legacy route's 50% buffer — the exact condition under which #678 was reachable. The chain-level guards (G1/G2) and the Hummingbot-side retry ownership are therefore **load-bearing** on the new SDK, not defense-in-depth. +- The legacy-SDK `closePosition` 404 mapping is obsolete: the new route uses `fetchMaybePosition` and 404s a missing position natively. +- The `getPositionInfo` null-contract fix (H3) was **ported**: the migrated implementation again swallowed every error to `null`, so it now uses `fetchMaybePosition` for a definitive "account does not exist" → `null`, and lets everything else throw. diff --git a/src/chains/solana/solana-error-parser.ts b/src/chains/solana/solana-error-parser.ts index f8fec795f3..6d58175a0c 100644 --- a/src/chains/solana/solana-error-parser.ts +++ b/src/chains/solana/solana-error-parser.ts @@ -90,8 +90,12 @@ const PROGRAM_ERROR_CODES: Record 0 ? `Program logs:\n${logs.slice(-12).join('\n')}` : '', + ] + .filter(Boolean) + .join('\n'); + + logger.error(errorMessage); + if (parsedError.type === 'SLIPPAGE_EXCEEDED') { + throw httpErrors.slippageExceeded(detail); + } + if (parsedError.type === 'INSUFFICIENT_BALANCE') { + throw httpErrors.insufficientBalance(detail); + } + throw httpErrors.simulationFailed(detail); + } + public async sendAndConfirmTransaction( tx: Transaction | VersionedTransaction, signers: Signer[] = [], @@ -1334,10 +1359,9 @@ export class Solana { const currentPriorityFee = priorityFeePerCU ?? (await this.estimateGasPrice()); // Always simulate transaction to get actual compute units - let computeUnitsToUse: number; + let computeUnitsToUse = this.config.defaultComputeUnits; + let simulationResult: { err: unknown; logs?: string[] | null; unitsConsumed?: number } | undefined; try { - let simulationResult; - if (tx instanceof Transaction) { // For regular transactions, simulate with the Transaction object const result = await this.connection.simulateTransaction(tx); @@ -1350,7 +1374,12 @@ export class Solana { }); simulationResult = result.value; } + } catch (error) { + logger.warn(`Failed to simulate for compute units: ${error.message}, using default`); + } + if (simulationResult) { + Solana.throwIfSimulationReturnedError(simulationResult); if (simulationResult.unitsConsumed) { // Add 10% margin for safety computeUnitsToUse = Math.ceil(simulationResult.unitsConsumed * 1.1); @@ -1358,13 +1387,8 @@ export class Solana { `Simulation consumed ${simulationResult.unitsConsumed} units, using ${computeUnitsToUse} with 10% margin`, ); } else { - // Fallback to default if simulation doesn't return units - computeUnitsToUse = this.config.defaultComputeUnits; logger.warn('Simulation did not return units consumed, using default'); } - } catch (error) { - logger.warn(`Failed to simulate for compute units: ${error.message}, using default`); - computeUnitsToUse = this.config.defaultComputeUnits; } const basePriorityFeeLamports = currentPriorityFee * computeUnitsToUse; @@ -1469,18 +1493,22 @@ export class Solana { // External wallets (hardware/Ledger): add the compute budget and sign any extra // keypairs, then sign the fee payer externally. const currentPriorityFee = priorityFeePerCU ?? (await this.estimateGasPrice()); - let computeUnitsToUse: number; + let computeUnitsToUse = this.config.defaultComputeUnits; + let simulationResult: { err: unknown; logs?: string[] | null; unitsConsumed?: number } | undefined; try { const sim = tx instanceof VersionedTransaction ? await this.connection.simulateTransaction(tx, { replaceRecentBlockhash: true, sigVerify: false }) : await this.connection.simulateTransaction(tx); - computeUnitsToUse = sim.value.unitsConsumed - ? Math.ceil(sim.value.unitsConsumed * 1.1) - : this.config.defaultComputeUnits; + simulationResult = sim.value; } catch (error) { logger.warn(`Failed to simulate for compute units: ${(error as Error).message}, using default`); - computeUnitsToUse = this.config.defaultComputeUnits; + } + if (simulationResult) { + Solana.throwIfSimulationReturnedError(simulationResult); + computeUnitsToUse = simulationResult.unitsConsumed + ? Math.ceil(simulationResult.unitsConsumed * 1.1) + : this.config.defaultComputeUnits; } let prepared: Transaction | VersionedTransaction; diff --git a/src/connectors/orca/orca.ts b/src/connectors/orca/orca.ts index 444c94fa6d..ae7d5c354f 100644 --- a/src/connectors/orca/orca.ts +++ b/src/connectors/orca/orca.ts @@ -1,5 +1,5 @@ import { fetchPositionsForOwner, setNativeMintWrappingStrategy, type WhirlpoolDeployment } from '@orca-so/whirlpools'; -import { fetchWhirlpool, fetchPosition } from '@orca-so/whirlpools-client'; +import { fetchWhirlpool, fetchPosition, fetchMaybePosition } from '@orca-so/whirlpools-client'; import { address, createSolanaRpc, mainnet, devnet } from '@solana/kit'; import { PublicKey } from '@solana/web3.js'; @@ -369,12 +369,16 @@ export class Orca { throw httpErrors.badRequest(`Invalid position address: ${positionAddress}`); } - try { - const positionInfo = await getPositionDetails(this.solanaKitRpc, positionAddress, this.deployment); - return positionInfo; - } catch (error) { - logger.error('Error getting position info:', error); + // null means "the position account does not exist" — a definitive on-chain + // answer that callers (position-info 404, close reconciliation in + // Hummingbot's LP executor) treat as "position closed". Anything else must + // throw: swallowing a transient RPC failure here serves "position closed" + // for a network blip, and an executor acting on that abandons a live, + // funded position while reporting success. + const maybePosition = await fetchMaybePosition(this.solanaKitRpc, address(positionAddress)); + if (!maybePosition.exists) { return null; } + return await getPositionDetails(this.solanaKitRpc, positionAddress, this.deployment); } } diff --git a/test/chains/solana/sendAndConfirmTransactionForWallet.test.ts b/test/chains/solana/sendAndConfirmTransactionForWallet.test.ts index c8a37ead5a..593612fda9 100644 --- a/test/chains/solana/sendAndConfirmTransactionForWallet.test.ts +++ b/test/chains/solana/sendAndConfirmTransactionForWallet.test.ts @@ -45,6 +45,39 @@ function versionedTx(): VersionedTransaction { return new VersionedTransaction(message); } +describe('Solana.sendAndConfirmTransaction compute simulation', () => { + const sendAndConfirm = (Solana.prototype as any).sendAndConfirmTransaction as ( + this: unknown, + tx: Transaction, + ) => Promise<{ signature: string; fee: number }>; + + it('does not broadcast when the compute-estimation simulation returned an error', async () => { + const _sendAndConfirmRawTransaction = jest.fn(); + const fakeThis = { + config: { defaultComputeUnits: 200000 }, + estimateGasPrice: jest.fn(async () => 0.1), + connection: { + simulateTransaction: jest.fn(async () => ({ + value: { + err: { InstructionError: [0, { Custom: 6018 }] }, + unitsConsumed: 10385, + logs: [ + 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc invoke [1]', + 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc failed: custom program error: 0x1782', + ], + }, + })), + }, + _sendAndConfirmRawTransaction, + }; + + await expect(sendAndConfirm.call(fakeThis, legacyTx())).rejects.toMatchObject({ + code: 'SLIPPAGE_EXCEEDED', + }); + expect(_sendAndConfirmRawTransaction).not.toHaveBeenCalled(); + }); +}); + describe('Solana.sendAndConfirmTransactionForWallet', () => { it('drops extra signers that carry the wallet pubkey (SDK dummy owner signers)', async () => { // Raydium's TxBuilder appends `owner.signer` to the signers it returns. For non-local diff --git a/test/chains/solana/solana-error-parser.test.ts b/test/chains/solana/solana-error-parser.test.ts index 7e4ae624d8..cdd2c765ea 100644 --- a/test/chains/solana/solana-error-parser.test.ts +++ b/test/chains/solana/solana-error-parser.test.ts @@ -110,6 +110,37 @@ describe('Solana Error Parser', () => { }); describe('Orca Whirlpool errors', () => { + it('should parse Orca token minimum error 6018 as slippage, not math overflow', () => { + const errorMessage = `Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc failed: custom program error: 0x1782`; + const result = parseSolanaError(errorMessage); + + expect(result.type).toBe('SLIPPAGE_EXCEEDED'); + expect(result.program).toBe('Orca Whirlpool'); + expect(result.errorCode).toBe(6018); + expect(result.message).toContain('minimum token amount'); + }); + + it('attributes a custom error to the FAILING program, not the first invoked one', () => { + // Simulation-shaped message: full logs open with prelude programs + // (ComputeBudget), and the Whirlpool failure comes later. The parser must + // consult Orca's table (6018 = TokenMinSubceeded → slippage), not fall + // through to the generic map's MATH_OVERFLOW. + const errorMessage = [ + 'Transaction simulation failed: ', + 'Error: {"InstructionError":[3,{"Custom":6018}]}', + 'Program Logs: Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc invoke [1]', + 'Program log: Error: TokenMinSubceeded', + 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc failed: custom program error: 0x1782', + ].join('\n'); + const result = parseSolanaError(errorMessage); + + expect(result.type).toBe('SLIPPAGE_EXCEEDED'); + expect(result.program).toBe('Orca Whirlpool'); + expect(result.errorCode).toBe(6018); + }); + it('should parse Orca slippage error via program ID string match', () => { const errorMessage = `Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc failed: custom program error: 0x178d`; const result = parseSolanaError(errorMessage); diff --git a/test/connectors/orca/orca.test.ts b/test/connectors/orca/orca.test.ts index 964e01cfa5..bc0ae36a34 100644 --- a/test/connectors/orca/orca.test.ts +++ b/test/connectors/orca/orca.test.ts @@ -17,6 +17,7 @@ jest.mock('@orca-so/whirlpools', () => ({ jest.mock('@orca-so/whirlpools-client', () => ({ fetchWhirlpool: jest.fn(), fetchPosition: jest.fn(), + fetchMaybePosition: jest.fn(), })); jest.mock('@solana/kit', () => ({ address: jest.fn((value: string) => value), @@ -27,7 +28,7 @@ jest.mock('@solana/kit', () => ({ })); import { fetchPositionsForOwner, setNativeMintWrappingStrategy } from '@orca-so/whirlpools'; -import { fetchPosition, fetchWhirlpool } from '@orca-so/whirlpools-client'; +import { fetchMaybePosition, fetchPosition, fetchWhirlpool } from '@orca-so/whirlpools-client'; import { Solana } from '../../../src/chains/solana/solana'; import { Orca } from '../../../src/connectors/orca/orca'; @@ -148,6 +149,7 @@ describe('Orca', () => { it('validates and reads a specific position without a wallet-bound SDK client', async () => { const info = { address: Keypair.generate().publicKey.toBase58() }; + (fetchMaybePosition as jest.Mock).mockResolvedValue({ exists: true }); (getPositionDetails as jest.Mock).mockResolvedValue(info); const orca = await Orca.getInstance('mainnet-beta'); await expect(orca.getPositionInfo(info.address, wallet.publicKey.toBase58())).resolves.toEqual(info); @@ -156,6 +158,27 @@ describe('Orca', () => { ); }); + it('returns null from getPositionInfo only when the account definitively does not exist', async () => { + (fetchMaybePosition as jest.Mock).mockResolvedValue({ exists: false }); + const orca = await Orca.getInstance('mainnet-beta'); + const positionAddress = Keypair.generate().publicKey.toBase58(); + await expect(orca.getPositionInfo(positionAddress, wallet.publicKey.toBase58())).resolves.toBeNull(); + expect(getPositionDetails).not.toHaveBeenCalled(); + }); + + it('propagates transient errors from getPositionInfo instead of reporting the position closed', async () => { + // Callers treat null as "position closed"; a swallowed RPC error here would + // let an LP executor abandon a live, funded position while reporting success. + (fetchMaybePosition as jest.Mock).mockRejectedValue(new Error('429 Too Many Requests')); + const orca = await Orca.getInstance('mainnet-beta'); + const positionAddress = Keypair.generate().publicKey.toBase58(); + await expect(orca.getPositionInfo(positionAddress, wallet.publicKey.toBase58())).rejects.toThrow('429'); + + (fetchMaybePosition as jest.Mock).mockResolvedValue({ exists: true }); + (getPositionDetails as jest.Mock).mockRejectedValue(new Error('RPC node behind')); + await expect(orca.getPositionInfo(positionAddress, wallet.publicKey.toBase58())).rejects.toThrow('RPC node behind'); + }); + it('keeps connector configuration public', async () => { const orca = await Orca.getInstance('mainnet-beta'); expect(orca.config).toBe(OrcaConfig.config); From 8f423d40e60183d5d64b1b3c320f9c1fe73d34cc Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 08:01:08 -0700 Subject: [PATCH 02/31] feat(solana): distinguish NOT_FOUND from UNCONFIRMED in transaction poll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/chains/solana/routes/poll.ts | 21 ++-- src/chains/solana/solana.ts | 24 ++++- src/schemas/chain-schema.ts | 5 +- test/chains/solana/routes/poll.test.ts | 131 +++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 8 deletions(-) create mode 100644 test/chains/solana/routes/poll.test.ts diff --git a/src/chains/solana/routes/poll.ts b/src/chains/solana/routes/poll.ts index b9f0266d70..6bc584294f 100644 --- a/src/chains/solana/routes/poll.ts +++ b/src/chains/solana/routes/poll.ts @@ -3,7 +3,7 @@ import { FastifyPluginAsync, FastifyInstance } from 'fastify'; import { PollRequestType, PollResponseType, PollResponseSchema } from '../../../schemas/chain-schema'; import { logger } from '../../../services/logger'; import { SolanaPollRequest } from '../schemas'; -import { Solana } from '../solana'; +import { Solana, TransactionResponseStatusCode } from '../solana'; import { parseSolanaError } from '../solana-error-parser'; export async function pollSolanaTransaction( @@ -16,13 +16,15 @@ export async function pollSolanaTransaction( try { const currentBlock = await solana.getCurrentBlockNumber(); - // Validate transaction signature format + // Validate transaction signature format. A malformed signature can never + // resolve, so report NOT_FOUND rather than a pending status a poller would + // wait on forever. if (!signature || typeof signature !== 'string' || !signature.match(/^[A-Za-z0-9]{43,88}$/)) { return { currentBlock, signature, txBlock: null, - txStatus: 0, + txStatus: TransactionResponseStatusCode.NOT_FOUND, fee: null, error: 'INVALID_INPUT: Invalid transaction signature format', txData: null, @@ -32,11 +34,16 @@ export async function pollSolanaTransaction( const txData = await solana.getTransaction(signature); if (!txData) { + // Null txData means either "seen but awaiting confirmation" or "unknown to + // the cluster" (dropped, or never received). Only the signature-status + // cache separates them: UNCONFIRMED is worth polling again, NOT_FOUND is + // terminal once the transaction's blockhash has expired. + const txStatus = await solana.getSignatureStatus(signature); return { currentBlock, signature, txBlock: null, - txStatus: 0, + txStatus, fee: null, error: null, txData: null, @@ -69,14 +76,16 @@ export async function pollSolanaTransaction( txData, }; } catch (err) { + // Transient failure (RPC error, etc.) — the transaction's fate is unknown, so + // report pending rather than NOT_FOUND: the caller should poll again, not give up. logger.error(`Error polling transaction ${signature}: ${(err as Error).message}`); return { currentBlock: await solana.getCurrentBlockNumber(), signature, txBlock: null, - txStatus: 0, + txStatus: TransactionResponseStatusCode.UNCONFIRMED, fee: null, - error: 'Transaction not found or invalid', + error: `Error polling transaction: ${(err as Error).message}`, txData: null, }; } diff --git a/src/chains/solana/solana.ts b/src/chains/solana/solana.ts index 84b3425a1e..7e0131995a 100644 --- a/src/chains/solana/solana.ts +++ b/src/chains/solana/solana.ts @@ -68,7 +68,8 @@ interface TokenAccount { value: any; } -enum TransactionResponseStatusCode { +export enum TransactionResponseStatusCode { + NOT_FOUND = -2, FAILED = -1, UNCONFIRMED = 0, CONFIRMED = 1, @@ -1190,6 +1191,27 @@ export class Solana { return txStatus; } + // Distinguishes a signature the cluster has seen from one it does not know at all. + // getTransaction (commitment 'confirmed') returns null for both a tx awaiting + // confirmation and a tx that was dropped, so pollers cannot tell them apart from + // txData alone. A signature that stays NOT_FOUND after its blockhash expires + // (~90s) can never land. + public async getSignatureStatus(signature: string): Promise { + const { value } = await this.connection.getSignatureStatuses([signature], { + searchTransactionHistory: true, + }); + const status = value[0]; + if (!status) { + return TransactionResponseStatusCode.NOT_FOUND; + } + if (status.err) { + return TransactionResponseStatusCode.FAILED; + } + // Seen by the cluster: 'processed', or confirmed/finalized racing ahead of + // getTransaction visibility — report unconfirmed and let the next poll resolve it. + return TransactionResponseStatusCode.UNCONFIRMED; + } + // returns the current block number async getCurrentBlockNumber(): Promise { return await this.connection.getSlot('processed'); diff --git a/src/schemas/chain-schema.ts b/src/schemas/chain-schema.ts index 6e7f49d896..dca9a10245 100644 --- a/src/schemas/chain-schema.ts +++ b/src/schemas/chain-schema.ts @@ -99,7 +99,10 @@ export const PollResponseSchema = Type.Object( currentBlock: Type.Number(), signature: Type.String(), txBlock: Type.Union([Type.Number(), Type.Null()]), - txStatus: Type.Number({ description: 'Transaction status: 1 = confirmed, 0 = pending, -1 = failed' }), + txStatus: Type.Number({ + description: + 'Transaction status: 1 = confirmed, 0 = pending, -1 = failed, -2 = not found (unknown to the chain; on Solana this is terminal once the transaction blockhash expires)', + }), fee: Type.Union([Type.Number(), Type.Null()]), error: Type.Union([Type.String({ description: 'Error info if failed: "TYPE (code): message"' }), Type.Null()]), txData: Type.Union([Type.Record(Type.String(), Type.Any()), Type.Null()]), diff --git a/test/chains/solana/routes/poll.test.ts b/test/chains/solana/routes/poll.test.ts new file mode 100644 index 0000000000..50440b4b86 --- /dev/null +++ b/test/chains/solana/routes/poll.test.ts @@ -0,0 +1,131 @@ +// Import shared mocks before importing app +import '../../../mocks/app-mocks'; + +import { pollSolanaTransaction } from '../../../../src/chains/solana/routes/poll'; +import { Solana, TransactionResponseStatusCode } from '../../../../src/chains/solana/solana'; + +// Mock the Solana class +jest.mock('../../../../src/chains/solana/solana', () => ({ + ...jest.requireActual('../../../../src/chains/solana/solana'), + Solana: { + getInstance: jest.fn(), + }, +})); + +const mockSolana = Solana as jest.Mocked; + +// 88-char base58-ish signature that passes the route's format validation +const VALID_SIGNATURE = '5'.repeat(88); + +describe('pollSolanaTransaction', () => { + const mockSolanaInstance = { + getCurrentBlockNumber: jest.fn(), + getTransaction: jest.fn(), + getTransactionStatusCode: jest.fn(), + getSignatureStatus: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockSolana.getInstance.mockResolvedValue(mockSolanaInstance as any); + mockSolanaInstance.getCurrentBlockNumber.mockResolvedValue(365795000); + }); + + it('returns CONFIRMED with fee when the transaction landed without error', async () => { + mockSolanaInstance.getTransaction.mockResolvedValue({ + slot: 365794000, + meta: { fee: 5000, err: null }, + }); + mockSolanaInstance.getTransactionStatusCode.mockResolvedValue(TransactionResponseStatusCode.CONFIRMED); + + const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); + + expect(result.txStatus).toBe(TransactionResponseStatusCode.CONFIRMED); + expect(result.txBlock).toBe(365794000); + expect(result.fee).toBe(5000 / 1e9); + expect(result.error).toBeNull(); + }); + + describe('null txData (not visible at confirmed commitment)', () => { + beforeEach(() => { + mockSolanaInstance.getTransaction.mockResolvedValue(null); + }); + + it('returns UNCONFIRMED when the cluster has seen the signature', async () => { + mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionResponseStatusCode.UNCONFIRMED); + + const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); + + expect(mockSolanaInstance.getSignatureStatus).toHaveBeenCalledWith(VALID_SIGNATURE); + expect(result.txStatus).toBe(TransactionResponseStatusCode.UNCONFIRMED); + expect(result.error).toBeNull(); + }); + + it('returns NOT_FOUND when the signature is unknown to the cluster', async () => { + mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionResponseStatusCode.NOT_FOUND); + + const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); + + expect(result.txStatus).toBe(TransactionResponseStatusCode.NOT_FOUND); + expect(result.error).toBeNull(); + }); + + it('returns FAILED when the signature status carries an error', async () => { + mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionResponseStatusCode.FAILED); + + const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); + + expect(result.txStatus).toBe(TransactionResponseStatusCode.FAILED); + }); + }); + + it('returns NOT_FOUND for a malformed signature, which can never resolve', async () => { + const result = await pollSolanaTransaction(null as any, 'mainnet-beta', 'not-a-signature'); + + expect(result.txStatus).toBe(TransactionResponseStatusCode.NOT_FOUND); + expect(result.error).toContain('INVALID_INPUT'); + expect(mockSolanaInstance.getTransaction).not.toHaveBeenCalled(); + }); + + it('returns UNCONFIRMED (not NOT_FOUND) on a transient RPC error so callers keep polling', async () => { + mockSolanaInstance.getTransaction.mockRejectedValue(new Error('RPC unavailable')); + + const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); + + expect(result.txStatus).toBe(TransactionResponseStatusCode.UNCONFIRMED); + expect(result.error).toContain('RPC unavailable'); + }); +}); + +describe('Solana.getSignatureStatus', () => { + const { Solana: RealSolana } = jest.requireActual('../../../../src/chains/solana/solana'); + + const makeInstance = (statusValue: any) => { + const instance = Object.create(RealSolana.prototype); + instance.connection = { + getSignatureStatuses: jest.fn().mockResolvedValue({ value: [statusValue] }), + }; + return instance; + }; + + it('maps a null status to NOT_FOUND', async () => { + const instance = makeInstance(null); + await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionResponseStatusCode.NOT_FOUND); + expect(instance.connection.getSignatureStatuses).toHaveBeenCalledWith([VALID_SIGNATURE], { + searchTransactionHistory: true, + }); + }); + + it('maps a status with err to FAILED', async () => { + const instance = makeInstance({ + err: { InstructionError: [2, { Custom: 6018 }] }, + confirmationStatus: 'confirmed', + }); + await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionResponseStatusCode.FAILED); + }); + + it('maps a processed (seen but unconfirmed) status to UNCONFIRMED', async () => { + const instance = makeInstance({ err: null, confirmationStatus: 'processed' }); + await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionResponseStatusCode.UNCONFIRMED); + }); +}); From f9d4ed71cdbaf5974ff1e634155aaa86d0763d9e Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 08:08:16 -0700 Subject: [PATCH 03/31] docs: rewrite retry-architecture as issues + final architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/retry-architecture.md | 446 +++++++++++-------------------------- 1 file changed, 135 insertions(+), 311 deletions(-) diff --git a/docs/retry-architecture.md b/docs/retry-architecture.md index 5d39360183..4c24759dc2 100644 --- a/docs/retry-architecture.md +++ b/docs/retry-architecture.md @@ -1,378 +1,202 @@ -# Retry Architecture: Gateway ↔ Hummingbot +# LP Close Retry Architecture -**Context:** [gateway#678](https://github.com/hummingbot/gateway/issues/678) — an Orca LP close failed with Whirlpool error `6018` (`TokenMinSubceeded`, misreported as `MATH_OVERFLOW`), the LP executor went terminally `FAILED` with the position still open on-chain, and subsequent stop requests returned 404. A proposed fix ([`040e99e`](https://github.com/hummingbot/gateway/commit/040e99ee705e7804066d9912d73751ba13a2a61a)) adds a 3-attempt rebuild-and-retry loop **inside** the gateway's Orca close route. This document formulates the canonical model for where retries live, evaluates that fix against it, and proposes changes. +**Origin:** [gateway#678](https://github.com/hummingbot/gateway/issues/678) — an Orca LP close failed with Whirlpool error `6018` (`TokenMinSubceeded`, misreported as `MATH_OVERFLOW`), the LP executor went terminally `FAILED` with the position still open on-chain, and subsequent stop requests returned 404. Fixing it properly required deciding **where retries live** across Gateway, the Hummingbot connector, the LP executor, the controller, and hummingbot-api — and auditing every layer against that model. This document describes the issues found and the architecture that now stands, spanning four repositories (gateway, hummingbot, hummingbot-api, condor). --- ## 1. The principle -> **Gateway is a stateless transaction oracle.** One HTTP request = one attempt at one on-chain action, built from fresh on-chain state, answered with either a confirmed result or a **typed error**. Hummingbot holds the position/order state, so Hummingbot owns retries — at three distinct altitudes, each answering a different question. +> **Gateway is a stateless transaction oracle.** One HTTP request = one attempt at one on-chain action, built from fresh on-chain state, answered with either a confirmed result or a **typed error**. Hummingbot holds the position and order state, so Hummingbot owns retries — at distinct altitudes, each answering a different question. -| Layer | Question it answers | Retry it owns | +| Layer | Question it answers | What it owns | |---|---|---| -| **Gateway** | "Did *this one attempt* land?" | Reads & transport only: RPC 429s, confirmation polling until blockhash expiry, tx-data re-fetch. Never re-submits a write. | -| **HB connector** (`gateway.py`, `gateway_base.py`) | "Should I ask Gateway for *another attempt*?" | `_execute_with_retry`: re-POSTs the operation (Gateway rebuilds fresh each time), max 10, only on `TRANSACTION_TIMEOUT` / `status: PENDING`. | -| **LP executor** (`lp_executor.py`) | "What does this failure mean for *the position lifecycle*?" | State-machine re-entry: `control_task` re-invokes `_create_position`/`_close_position` while state stays `OPENING`/`CLOSING`. Reconciles already-succeeded outcomes. | -| **Controller** (`lp_rebalancer.py`) | "What does this failure mean for *the strategy*?" | Re-creates a fresh executor after a `FAILED` one (unbounded, 1 s tick). **Bot topology only** — absent on the `manage_executors` path (§5). | - -This principle is already written into the code on both sides: - -- Gateway `raydium/clmm-routes/removeLiquidity.ts:114`: `// Transaction pending, return for Hummingbot to handle retry` → returns `status: 0` + signature. -- Gateway `solana.ts:1401-1408`: confirmation timeout throws a 504 **including the signature** so "the caller can reconcile." -- Hummingbot `lp_executor.py:594-598` docstring: "Retry logic is handled by the connector." +| **Gateway** | "Did *this one attempt* land?" | Reads and transport only: RPC 429s, confirmation polling until blockhash expiry, typed error classification, pre-broadcast simulation rejection. Never re-submits a write. | +| **Connector** (`gateway_base.py`, `gateway.py`) | "Should I ask Gateway for *another attempt*?" | `_execute_with_retry`: re-POSTs the operation (Gateway rebuilds fresh each time). Timeouts retry for every operation; additional error codes are an explicit, per-operation opt-in. | +| **LP executor** (`lp_executor.py`) | "What does this failure mean for *the position lifecycle*?" | State-machine re-entry with a bounded budget and exponential backoff; reconciliation of already-succeeded outcomes; the terminal close type. | +| **Controller** (`lp_rebalancer.py`) | "What does this failure mean for *the strategy*?" | Re-creates a fresh executor with freshly computed bounds; halts instead of stacking exposure over an unresolved position. | +| **hummingbot-api / condor** | "What survives the process, and who is told?" | Durable orphan records, listing and resolution endpoints, agent-facing warnings. | ```mermaid flowchart TB - subgraph HB["Hummingbot (stateful — owns retries)"] - C["Controller: lp_rebalancer
strategy-level: re-create executor after FAILED
∞ retries, 1s tick"] - E["LP Executor: lp_executor
lifecycle: state machine re-entry + reconciliation
OPENING/CLOSING re-attempts via control_task"] - K["Connector: gateway.py / gateway_base.py
operation-level: _execute_with_retry
10 re-POSTs, only on TRANSACTION_TIMEOUT / PENDING"] + subgraph HB["Hummingbot — stateful, owns retries"] + C["Controller: lp_rebalancer
strategy: re-create after clean FAILED,
halt over unresolved position
"] + E["LP executor: lp_executor
lifecycle: CLOSING re-entry, max_retries + 1 attempts,
exponential backoff (2ⁿ s, cap 30 s)
"] + K["Connector: gateway_base / gateway
operation: _execute_with_retry —
timeouts always; close opts in to slippage-class errors,
inner budget 3
"] end - subgraph GW["Gateway (stateless — one attempt per request)"] + subgraph GW["Gateway — stateless, one attempt per request"] R["Route: build tx from FRESH on-chain state
quote → simulate → sign → broadcast"] - S["Chain layer: solana.ts
transport retries only
429 interceptor · confirm-poll to blockhash expiry · tx-data refetch"] + S["Chain layer: typed errors, 429 interceptor,
confirm-poll to blockhash expiry,
pre-broadcast simulation guard"] end - C -->|CreateExecutorAction| E - E -->|"_clmm_close_position(max_retries=10)"| K - K -->|"POST /clmm/close-position
(each retry = fresh rebuild)"| R + A["hummingbot-api / condor
durable orphan records, stop semantics, agent visibility"] + C --> E + E --> K + K -->|"each retry = fresh rebuild"| R R --> S - S -->|"CONFIRMED (status 1) · PENDING (status 0 + sig)
or typed error [code: X]"| K - K -->|result or re-raised exception| E - E -->|close_type on terminal| C + S -->|"CONFIRMED · typed error [code: X]"| K + K --> E + E -->|terminal close_type| C + E -.->|persisted final state| A ``` -The key property this preserves: **a connector-level retry already gets fresh state for free**, because every POST re-runs the route's fetch → quote → build → simulate pipeline. Gateway-side write retries duplicate that loop one layer down, per-connector. +The property this preserves: **a connector-level retry gets fresh state for free**, because every re-POST re-runs the route's fetch → quote → build → simulate pipeline. A retry loop inside a Gateway route would duplicate that one layer down, per connector, invisible to the strategy, and stacking under the connector's own loop. --- -## 2. Retry ownership matrix (as implemented today) +## 2. Issues found + +The #678 investigation and the adversarial review of the fix surfaced the following defects. Each is fixed in this PR family unless marked as an accepted residual (§4). + +### Gateway + +1. **Misattributed on-chain errors.** Orca `6018` was missing from the error table (reported as `MATH_OVERFLOW`), and the parser attributed custom errors to the *first* program in the log — simulation logs open with a ComputeBudget prelude, so the failing DEX program's table was never consulted. Errors are now attributed to the program on the `failed: custom program error` line, and `6018` maps to `SLIPPAGE_EXCEEDED`. +2. **Doomed transactions were broadcast anyway.** The compute-estimation simulation's `err` was ignored (only `unitsConsumed` was read), so a transaction guaranteed to fail was signed, broadcast, and paid fees. Both send paths now reject a failed simulation with a typed 400 **before broadcast** — a failed attempt costs nothing. +3. **Transient read errors served as definitive closure.** `Orca.getPositionInfo` swallowed every error into `null`, which the route surfaced as the position-specific 404 — so an RPC blip inside Gateway read as "position closed". It now returns `null` only for a definitive account-does-not-exist result and rethrows everything else. +4. **A dropped transaction was indistinguishable from a pending one.** `getTransaction` (commitment `confirmed`) returns null both for a transaction awaiting confirmation and for one the cluster has never seen, and `/poll` reported both as `txStatus 0` (pending) — forever. The poll now consults the signature-status cache (with history search) and reports an unknown signature as **`NOT_FOUND` (-2)**, which is terminal once the transaction's blockhash has expired (~90 s). +5. **Post-SDK-migration slippage exposure.** The migrated Orca close route quotes withdrawal minimums at the configured `slippagePct` (~1%) rather than the legacy 50% buffer — exactly the condition under which the #678 race is reachable. The guards above are load-bearing, not defense-in-depth. + +### Hummingbot connector -What actually protects each operation, per layer. ✅ = bounded retry exists, ♻️ = unbounded, ❌ = none (failure is terminal at that layer). +6. **The retry classifier was operation-blind.** `SLIPPAGE_EXCEEDED`/`SIMULATION_FAILED` were globally non-retryable — correct for a swap (re-submitting the same intent re-fails or double-spends), wrong for a close, where every re-POST rebuilds amounts from fresh state and success consumes the position account (idempotent). `_execute_with_retry` now takes per-operation `retryable_error_codes`; close opts in to `{SLIPPAGE_EXCEEDED, SIMULATION_FAILED, TX_NOT_CONFIRMED}`. +7. **The landed-but-failed shape had no error code.** A broadcast transaction that failed on-chain raised with no `[code:]` marker at all, so the flagship #678 failure shape was unretryable even with an opt-in. It now raises typed `TX_NOT_CONFIRMED`. +8. **`get_position_info` swallowed every exception into `None`**, and the executor read `None` as "already closed" → `COMPLETE` — a transient RPC error during close could **abandon a live position while reporting success**. The contract is now: `None` only for a position-specific 404; everything else re-raises. Three coupled requirements: (a) the match is position-specific, because the HTTP client stamps "(Not Found)" on *every* 404 — a missing route after a redeploy read as "position gone"; (b) existence decisions use an **uncached** read (`get_position_info_fresh`) — the 5 s TTL cache stores `None` like any value, so one cached 404 masqueraded as several independent confirmations; (c) external-close detection requires the position to have been **seen on-chain at least once** plus 3 consecutive fresh misses. +9. **Pending-transaction polling was unbounded.** `update_order_status` polled any in-flight order at 1 s forever; combined with issue 4, a dropped transaction never resolved — the order never failed and burned an RPC call per second until restart. The connector now treats `NOT_FOUND` as transient while the order is younger than `TX_NOT_FOUND_DEADLINE` (120 s, past blockhash validity) and afterwards feeds each miss to the order tracker's existing lost-order machinery, which fails the order after repeated consecutive misses. `PENDING` remains unbounded by design: the chain has seen the transaction, so it can still confirm. -| Operation | Gateway (writes) | Connector | Executor | Controller | -|---|---|---|---|---| -| **Swap** | ❌ single-shot | ✅ 10× (`_create_order`, `gateway.py:331`) — timeout only | ✅ 10× (`OrderExecutor:334-342`, any failure event) | ♻️ re-create (`lp_rebalancer.py:487`) | -| **Open position / add liq (CLMM)** | ❌ single-shot | ✅ 10× (`_clmm_add_liquidity`, `gateway.py:905`) | ❌ `_handle_create_failure:415` → `FAILED` (deliberate: a blind retry could mint a 2nd position) — but flips to `CLOSING` to recover funds if the add landed | ♻️ re-create same side (`lp_rebalancer.py:543-563`) | -| **Close position (CLMM)** | ❌ single-shot *(the proposed fix makes Orca 3×)* | ✅ 10× (`_clmm_close_position`, `gateway.py:1064`) — **timeout only** | ❌ **`_handle_close_failure:593-602` → `FAILED`, position still live on-chain** | ❌ **nothing — opens a *new* position instead** | -| AMM add/remove, partial remove | ❌ | ❌ direct await, no retry | ❌ | ❌ | -| Pending-tx polling | confirm-poll to blockhash expiry | ♻️ `update_order_status` 1 s forever | — | — | -| Reads (position/pool info) | 429 interceptor | 5 s TTL cache; **exceptions swallowed → `None`** (`gateway.py:1226-1235`) | string-matched reconciliation | — | +### LP executor -Two structural observations fall out of this table: +10. **Close failure was terminal on the first error.** `_handle_close_failure` jumped straight to `FAILED` with the position still open. It now counts the attempt, arms exponential backoff (2ⁿ s, capped 30 s — so a Gateway restart spans a few retries instead of burning the whole budget in seconds), and stays `CLOSING`; the family hook `evaluate_max_retries` decides termination after `max_retries + 1` attempts. +11. **Retry stacking.** With the executor loop added, the connector's default 10 inner retries would stack into ~O(n²) submissions per close. The connector's inner budget for close is `min(3, max_retries)`; the executor owns the policy. +12. **The wrong terminal close type.** `FAILED` in the executor family means "abnormal end with *no residual exposure*" — everything downstream (hold store, PnL, dashboards) assumes it. Terminating an exhausted close as `FAILED` forced a parallel side-channel at every layer. An exhausted close with the position still on-chain now terminates as an **involuntary `POSITION_HOLD`** with `hold_reason: "close_retries_exhausted"` and a zero-amount marker order carrying the position address — riding the existing hold machinery (DB-recovered on the API path) instead of a bespoke flag. `FAILED` is reserved for exhaustion with nothing left on-chain (e.g. an open rejected at simulation). -1. **Swaps are retried at three layers; closes at one.** The close's only retry (connector, 10×) fires solely on `TRANSACTION_TIMEOUT`. Every other error — including the stale-minimums slippage error at the heart of #678 — is classified `FAIL_IMMEDIATE` (`gateway_base.py:42-52`, `NON_RETRYABLE_ERROR_CODES` = `SIMULATION_FAILED`, `INSUFFICIENT_BALANCE`, `SLIPPAGE_EXCEEDED`, `INVALID_PARAMS`, `NO_ROUTE_FOUND`). -2. **The one operation that is *safest* to retry is the one with the least retry coverage.** See §3. +### Controller, hummingbot-api, condor + +13. **The controller stacked exposure over a live position.** After a terminal executor still holding a position, `lp_rebalancer` created a fresh executor — a fresh `lp_executor` cannot adopt an existing position (it always mints a new one), so this doubled the funded exposure. It now halts and logs until the orphan is resolved. +14. **Stopping a terminal executor returned 404.** The API's completion handler pops the executor from memory within one tick, so every stop against a terminal executor hit the "unknown id" branch — the #678 dead-end. Stop is now DB-aware: any DB-known, not-in-memory executor returns `already_terminated` with its final `close_type`, `position_address`, and `hold_reason`; 404 is reserved for ids the database has never seen. +15. **Failures were invisible to agents.** The condor tick prompt listed only `RUNNING` executors, so a terminal executor with a live position *vanished from view*. The provider now surfaces orphaned executors with a warning, and `manage_executors` gained `orphaned` and `resolve_orphan` actions. +16. **Orphans had no durable record or resolution path.** The persisted final state now carries the orphan shape; `GET /executors/positions/orphaned` lists candidates (involuntary holds, legacy `FAILED`-with-position, and `SYSTEM_CLEANUP` LP executors from an API restart — the latter flagged `needs_onchain_reconciliation` since no final state was persisted); `POST /executors/{id}/resolve-orphan` marks a recovered position so it stops surfacing. --- -## 3. Why close differs from swap and open: idempotency +## 3. The architecture -The proposed fix's justification for retrying *only* Orca close inside gateway is real, but it answers "where is retry **safe**", not "where should retry **live**". +### 3.1 Why close retries and open does not | Operation | If the outcome is uncertain and you blindly re-submit… | Blind-retry safe? | |---|---|---| -| Swap | Second swap also executes → **double spend** | ❌ must verify the signature first | -| Open position | Second position minted (fresh mint each attempt) → duplicate exposure | ❌ | -| Add liquidity | Deposits twice | ❌ | -| Partial remove | Removes twice | ❌ | -| **Close position** | Success **consumes the position account**; a second close fails cleanly with "position not found" | ✅ naturally idempotent | -| Collect fees | Second collect transfers ~0 | ✅ mostly | - -So a bounded close-until-closed loop is *correct*. The question is only which layer runs it — and the layered answer is strictly better, because: +| Swap | Second swap also executes → double spend | ❌ | +| Open position | Second position minted → duplicate exposure | ❌ | +| **Close position** | Success consumes the position account; a second close fails cleanly with "position not found" | ✅ idempotent | -- **Fresh state is free at the connector layer too.** Each re-POST rebuilds quote, minimums, and blockhash. The only thing gateway-side retry buys is a few hundred ms less latency between attempts. -- **The reconciliation already exists upstream.** `lp_executor._close_position:454-486` pre-flights `get_position_info`, and maps "already closed" / "not found" → `_emit_already_closed_event()` → `COMPLETE`. The gateway fix re-implements this same reconciliation inside one route, for one connector. -- **Per-connector divergence.** There are six close-position routes (orca, meteora, raydium, pancakeswap, pancakeswap-sol, uniswap). A gateway-side loop fixes one; a hummingbot-side policy fixes all of them at once, plus every future connector. -- **Retry stacking.** The connector still wraps the route in a 10× loop. With the gateway loop inside, a timeout-flavored failure can trigger up to 10 × 3 = 30 build/submit cycles per close, invisible to the strategy. Neither layer sleeps between attempts. -- **Policy visibility.** Retry counts, backoff, and give-up conditions are strategy decisions. Buried in a route they can't be configured per-executor, surfaced in `get_custom_info`, or reasoned about from the bot. +The same price move that makes a close fail (stale withdrawal minimums) also makes an open fail (stale deposit maximums), but the recovery is asymmetric: ---- +- **Close**: the intent — "remove whatever is in this position" — stays valid at any price. Retry with a fresh quote, at the connector (fast path) and the executor (paced re-entry). +- **Open**: the intent — a range and a base/quote split computed at the old price — is stale. The executor fails cleanly with nothing on-chain (`FAILED`), and the **controller** re-decides: the next cycle recomputes bounds around the current price and creates a fresh executor. (One guard: if the add actually landed and only the bookkeeping after it threw, the executor flips to `CLOSING` to recover the funds instead of stranding them.) -## 4. Anatomy of issue #678 +### 3.2 The close lifecycle ```mermaid sequenceDiagram - participant CT as Controller - participant EX as LP Executor + participant EX as LP executor participant CN as Connector - participant GW as Gateway (Orca route) - participant SOL as Solana - - EX->>EX: state = CLOSING - EX->>CN: _clmm_close_position (max_retries=10) - CN->>GW: POST /clmm/close-position (attempt 1) - GW->>SOL: fetch position + pool, build tx ONCE
(minimums from now-stale quote) - GW->>SOL: simulate (compute units) - Note over GW: sim err ignored — only unitsConsumed read - GW->>SOL: broadcast - SOL-->>GW: landed on-chain but FAILED: custom error 0x1782 (6018) - GW-->>CN: 400 [code: SIMULATION_FAILED]
"MATH_OVERFLOW" (6018 missing from Orca map) - Note over CN: SIMULATION_FAILED ∈ NON_RETRYABLE
→ FAIL_IMMEDIATE (gateway_base.py:782) - CN-->>EX: re-raise - EX->>EX: _handle_close_failure → state = FAILED - EX->>EX: next tick: close_type = FAILED, stop() - Note over EX: position STILL OPEN on-chain - CT->>CT: sees FAILED → creates NEW executor,
opens a SECOND position (lp_rebalancer.py:543) - Note over CT,EX: stop request for old executor → 404
(popped from API memory on completion — see §5) + participant GW as Gateway + participant SOL as Chain + + loop each CLOSING re-entry (≤ max_retries + 1, backoff 2ⁿ s cap 30 s) + EX->>CN: get_position_info_fresh (uncached) + alt position definitively absent (seen on-chain before + 3 fresh misses) + EX->>EX: already closed → COMPLETE + else position live or read transiently failed + EX->>CN: close position + loop ≤ 3 inner attempts on SLIPPAGE_EXCEEDED / SIMULATION_FAILED / TX_NOT_CONFIRMED + CN->>GW: POST close-position + GW->>SOL: fetch fresh state → quote → build → simulate + alt simulation fails + GW-->>CN: typed 400, pre-broadcast — no fee spent + else + GW->>SOL: broadcast + confirm + GW-->>CN: CONFIRMED or typed error + end + end + CN-->>EX: result or typed exception + end + end + Note over EX: budget exhausted, position still live →
involuntary POSITION_HOLD (hold_reason: close_retries_exhausted,
zero-amount marker order carrying position_address) ``` -Note the punchline: **the proposed error-mapping fix alone doesn't change the outcome.** Mapping 6018 → `SLIPPAGE_EXCEEDED` produces a clearer message, but `SLIPPAGE_EXCEEDED` is *also* in `NON_RETRYABLE_ERROR_CODES` — correct for a swap (retrying the same quote re-fails), wrong for a close (the fix *is* rebuilding, which a re-POST does). This is why the retry decision must be **operation-aware**, and why it lives naturally in the connector, which knows which operation it is running. +### 3.3 Terminal semantics ---- +```mermaid +stateDiagram-v2 + [*] --> OPENING + OPENING --> IN_RANGE: position minted + OPENING --> FAILED: open failed, nothing on-chain
(controller re-decides with fresh bounds) + OPENING --> CLOSING: add landed but bookkeeping threw
(recover the funds) + IN_RANGE --> CLOSING: limit price hit / stop requested + OUT_OF_RANGE --> CLOSING: limit price hit / stop requested + IN_RANGE --> OUT_OF_RANGE + OUT_OF_RANGE --> IN_RANGE + CLOSING --> CLOSING: close attempt failed,
budget remains — backoff, re-enter + CLOSING --> COMPLETE: closed (or confirmed already closed) + CLOSING --> POSITION_HOLD: budget exhausted, position live —
hold_reason set, marker order carries address + POSITION_HOLD --> [*] + COMPLETE --> [*] + FAILED --> [*] +``` -## 5. The second topology: `manage_executors` has no controller +- **`FAILED`** = abnormal end with **nothing left on-chain**. The controller may create a replacement. +- **`POSITION_HOLD` + `hold_reason`** = residual exposure that could not be closed. Voluntary holds (`keep_position=True`) and involuntary holds are the same close type, distinguished only by the reason. The zero-amount marker order makes the hold visible to the durable hold store without corrupting spot accounting (a still-open position's pool balances are not returned tokens). +- Consumers key off `hold_reason`, not close type: the rebalancer halts and skips accounting; the API's orphan listing is a view over involuntary holds; condor warns the agent. +- Exception: a **force-stop** with a live position still terminates `FAILED` — the close may be in flight at the shutdown deadline, so the legacy `FAILED`-with-position orphan class covers it. -The stack in §1 describes a **deployed bot** running the `lp_rebalancer` controller. But users (and condor agents) also open and close LP executors directly through the `manage_executors` MCP tool, and on that path the topology is different: **the executor runs inside the hummingbot-api process, and the controller layer does not exist.** +### 3.4 Two topologies, one orphan lifecycle + +The executor is the only layer present in both deployment topologies, which is why the bounded close re-entry lives there. ```mermaid flowchart TB subgraph TA["Topology A — deployed bot"] - A0["lp_rebalancer controller
♻️ re-creates after FAILED"] --> A1["ExecutorOrchestrator"] --> A2["LPExecutor"] + A0["lp_rebalancer controller"] --> A1["ExecutorOrchestrator"] --> A2["LPExecutor"] end subgraph TB2["Topology B — manage_executors (condor / API)"] - B0["condor agent tick / Telegram user
⚠️ prompt lists RUNNING executors only"] --> B1["manage_executors MCP tool
condor mcp_servers/hummingbot_api"] --> B2["hummingbot-api ExecutorService
in-process dict _active_executors
❌ FAILED handled same as COMPLETE"] --> B3["LPExecutor
(same class, runs in the API process)"] + B0["condor agent / API user"] --> B1["manage_executors MCP tool"] --> B2["hummingbot-api ExecutorService
(no controller — controller_id is a label)"] --> B3["LPExecutor"] end A2 --> GW["Gateway"] B3 --> GW ``` -Facts, from tracing condor + hummingbot-api: - -- **`controller_id` is a label, not an owner.** `ExecutorService.create_executor` (`hummingbot-api/services/executor_service.py:424-488`) instantiates `LPExecutor` directly against a trading-interface shim and stores `controller_id` (default `"main"`, `:455`; condor agents pass their agent id, `condor/agents/prompts.py:350`) purely for filtering. No `ExecutorOrchestrator`, no `ControllerBase` is ever involved. -- **Terminal means forgotten.** The 1 s `_control_loop` (`executor_service.py:293-313`) detects `is_closed` and calls `_handle_executor_completion` (`:652-680`), which **pops the executor from memory** (`:657`), persists the final state, and returns. `CloseType.FAILED` takes the same path as `COMPLETE` — no re-creation, no alert, no orphan flag. -- **The #678 stop-404, precisely.** `stop_executor` looks up the in-memory dict and 404s on a miss (`executor_service.py:630-632`). Because the completion handler pops within one tick of the executor closing, the "already closed → 400" branch (`:634-635`) is effectively dead code — a stop against any terminal executor returns **404**. Meanwhile `GET /executors/{id}` falls back to the DB, so the agent sees an executor that is *findable but un-stoppable*. A second orphan source needs no gateway error at all: an API restart rewrites every RUNNING row to `TERMINATED/SYSTEM_CLEANUP` (`executor_service.py:229-259`) while the on-chain position lives on. -- **The condor agent cannot see the failure.** The executor summary injected into the agent's tick prompt lists **only `status == "RUNNING"`** executors (`condor/condor/agents/providers/executors.py:55-67`). A `FAILED` LP executor doesn't raise a flag — it *vanishes from view*, with a live position still on-chain. Nothing in condor branches on `close_type`; recovery exists only as prose in `guides/lp_executor.md:174-183` ("close the position via the DEX UI… manually update the executor status in the database"). - -Consequence for the retry model: in Topology B the ownership stack **stops at the executor**. The executor layer is the only layer present in both topologies — which makes the executor-level close retry (E1 below) the keystone fix, and means `lp_rebalancer`'s recovery duties (re-create / reconcile orphans) need a topology-B home: the `ExecutorService` completion handler and the condor data provider, not a controller. - ---- - -## 6. Verdict on commit `040e99e` - -| Piece of the fix | Verdict | Why | -|---|---|---| -| Map Orca `6018` → `SLIPPAGE_EXCEEDED` (`solana-error-parser.ts`) | ✅ **merge** | Pure correctness. Typed errors are Gateway's core job. | -| Reject simulation `err` before broadcast (`solana.ts`, both send paths) | ✅ **merge** | Fail-fast with a typed 400 instead of broadcasting a doomed tx and paying fees. Stateless. Benefits every connector. | -| 3-attempt rebuild-and-retry loop in `closePosition.ts` | ❌ **don't merge — relocate to Hummingbot** | Write-retry policy inside a stateless service; Orca-only; stacks under the connector's 10×; duplicates executor reconciliation. | -| In-route reconciliation (position gone → check attempted signature → return success) | ❌ **don't merge** | Same capability already exists at `lp_executor.py:454-486` (pre-flight) and can be reached on the next CLOSING re-entry. Keeping one reconciliation point avoids two layers disagreeing. | - ---- - -## 7. Proposed changes - -### The state machine, before and after +When a close exhausts its budget: ```mermaid -stateDiagram-v2 - direction LR - state "Current" as cur { - [*] --> CLOSING - CLOSING --> COMPLETE: closed / already closed - CLOSING --> FAILED: ANY close error
(after connector 10x timeout-only) - FAILED --> [*]: stop() — position still live - } - state "Proposed" as prop { - [*] --> CLOSING2 - CLOSING2: CLOSING - CLOSING2 --> COMPLETE2: closed / already closed
(pre-flight reconciles landed tx) - CLOSING2 --> CLOSING2: close error and retries left —
stay CLOSING, control_task re-enters
(fresh rebuild via re-POST) - CLOSING2 --> FAILED2: retries exhausted - COMPLETE2: COMPLETE - FAILED2: FAILED — position flagged as orphan,
controller must not blind-reopen - } +flowchart TD + T["Executor terminates POSITION_HOLD
hold_reason: close_retries_exhausted"] --> P["hummingbot-api persists final state
with orphan shape"] + T --> H["Controller (topology A) halts:
no new positions over the orphan"] + P --> L["GET /executors/positions/orphaned
lists the candidate"] + P --> W["condor tick prompt warns the agent"] + L --> R["Operator/agent closes the position
via gateway tools (remove liquidity by address —
a fresh lp_executor CANNOT adopt it)"] + R --> S["POST /executors/{id}/resolve-orphan
clears the listing and warnings"] + S --> X["Restart the controller —
its halt is in-memory; restart is the
acknowledgment that recovery is done"] ``` -> **Terminal state superseded by §10:** exhaustion with the position still on-chain now terminates as an **involuntary `POSITION_HOLD`** (`hold_reason: "close_retries_exhausted"` + a zero-amount marker order carrying the position address) rather than `FAILED`; `FAILED` is reserved for exhaustion with nothing left on-chain. The `CLOSING → CLOSING` retry loop above is unchanged. - -### Hummingbot — the real fix - -**E1 (core). Bounded close re-entry in the executor.** `_handle_close_failure` (`lp_executor.py:593-602`): instead of jumping to `FAILED`, increment `_current_retries` and *return with state still `CLOSING`*. `control_task` (`lp_executor.py:163-167`) already re-invokes `_close_position` every tick for exactly this purpose — the machinery exists and is currently unreachable (`data_types.py:159-166` deliberately preserves `CLOSING` across updates). Bound it against `_max_retries` (implemented inline in the handler rather than via the inert `evaluate_max_retries` — same outcome, one owner); go `FAILED` only on exhaustion, with exponential backoff (capped 30 s) between attempts so instant transport failures (e.g. a Gateway restart) can't burn the budget in seconds. The pre-flight at `_close_position:454-486` gives every re-entry the timeout-but-landed reconciliation for free. Since the executor owns the retry policy, the connector's inner budget for close is cut to 3 — leaving it at 10 would stack into ~O(n²) submissions per close, the same stacking §3 rejects. This is the same loop the gateway fix implements, one layer up, for **all** connectors. - -**H1. Operation-aware retry classification in the connector.** `_execute_with_retry` (`gateway_base.py:674-753`) treats `SLIPPAGE_EXCEEDED`/`SIMULATION_FAILED` as fail-immediate globally. Add a `retryable_error_codes` parameter; `_clmm_close_position` opts in to `{SLIPPAGE_EXCEEDED, SIMULATION_FAILED, TX_NOT_CONFIRMED}` because each re-POST rebuilds from fresh state and close is idempotent (§3). `TX_NOT_CONFIRMED` is the newly-typed code on the landed-but-failed branch (`status: -1`), which previously raised with no `[code:]` at all — meaning the exact #678 failure shape (tx broadcast, failed on-chain) was unretryable even with the opt-in. Swaps and opens keep today's conservative set. (E1 alone would also cover this at backoff granularity; H1 makes the fast path fast.) - -**H3 (latent bug, load-bearing).** `get_position_info` (`gateway.py:1226-1235`) swallows every exception → `None`, and the executor reads `None` as "position already closed" → `COMPLETE` (`lp_executor.py:462-468`). A transient RPC error during close can therefore **abandon a live position while reporting success** — worse than #678. Three coupled requirements, each necessary: -- `None` only for a **position-specific** 404 ("Position not found…"/"Position closed…"); a bare "not found" match also catches the HTTP client's "(Not Found)" suffix on unrelated 404s (missing route after a redeploy, missing wallet file) and would report a live position as gone. Everything else re-raises. -- Existence decisions must use an **uncached** read (`get_position_info_fresh`): the 5 s `@async_ttl_cache` stores `None` like any value, so one cached 404 would be served for several consecutive 1 s ticks and masquerade as independent confirmations. -- External-close detection additionally requires the position to have been **seen on-chain at least once** plus 3 consecutive fresh misses — a not-found streak on a just-created position is RPC lag, not a close. - -**C1. Controller must not blind-reopen over a live position.** `lp_rebalancer.py:543-563` re-creates after `FAILED` without checking `custom_info["position_address"]`. If the failed executor still holds a live position address: halt-and-log instead of opening a second position. The `position_hold` accounting skip at `514-518` ("no tokens deposited" — false for close failures) is deliberately retained: with the halt in place, no replacement executor is created, so there is no round trip to book; booking one would require amounts the failed close never produced. Known limitation: the halt is in-memory only — a bot restart clears it and the controller will blind-reopen; the durable guard for that case is the orphan flag + listing at the API layer (A1/A4). - -**H2 (hygiene).** Add small sleep/backoff between `_execute_with_retry` attempts (today pacing relies entirely on the blocking POST), and expose `current_retries`/`max_retries` in `LPExecutor.get_custom_info` the way `OrderExecutor` does (`order_executor.py:353-354`). - -### hummingbot-api & condor — the controller's duties, without a controller (Topology B) - -**A1. Flag orphans at the one place every executor passes through.** `_handle_executor_completion` (`hummingbot-api/services/executor_service.py:652-680`): when `close_type == FAILED` and `custom_info` still carries a `position_address`, persist an **orphaned-position flag** on the executor record (and log at error level) instead of treating it like any other completion. Expose orphans via `/executors/search` and the positions summary so both the dashboard and agents can query them. This is `lp_rebalancer`'s "skip accounting / react to FAILED" role, relocated to the layer that actually owns Topology-B executors. - -**A2. Make stop DB-aware instead of 404.** `stop_executor` (`executor_service.py:630-632`) should fall back to the DB the way `get_executor` (`:563-592`) already does: a stop against a terminal executor returns its final state as a no-op success ("already terminated, close_type=FAILED, orphaned position at X…"), and 404 is reserved for ids that never existed. This directly removes the second #678 symptom, and turns a dead end into an actionable answer for the agent. - -**A3. Let the condor agent see failures.** The tick-prompt executor summary (`condor/condor/agents/providers/executors.py:55-67`) filters to RUNNING; include terminal-with-orphan executors (from A1, or minimally any recent `close_type=FAILED` with a `position_address`) in the narrative summary so the agent's next tick can react — retry the close via `manage_executors`/`manage_clmm`, or alert the user. Today the failure is silently invisible. - -**A4. Codify chain-vs-tracked reconciliation.** The guides already instruct agents in prose to cross-check `positions_owned` / `get_portfolio_overview(include_lp_positions=True)` against executor `custom_info.position_address` (`condor guides/lp_executor.md:176-183`). Promote this to code: a hummingbot-api endpoint (or condor routine) that diffs on-chain positions against tracked executors and reports strays — this also catches the API-restart orphan class (`SYSTEM_CLEANUP`), which involves no gateway error at all. - -### Gateway — keep it stateless - -**G1. Merge** the `6018` error mapping from `040e99e`. - -**G2. Merge** the simulation-`err` rejection from `040e99e` (both `sendAndConfirmTransaction` paths). This converts most stale-state failures into a pre-broadcast typed 400 — no fee spent, immediately retryable upstream. - -**G3. Drop** the in-route retry/reconciliation loop from `closePosition.ts`. The route stays: fetch fresh → build → send → report, with `404 Position not found` continuing to serve as the upstream "already closed" signal. +Also listed as orphan candidates: legacy `FAILED` executors whose final state carries a position address (force-stop stragglers), and `SYSTEM_CLEANUP` LP executors from an API restart (position address unknown — reconcile against on-chain positions). -### Rollout order +### 3.5 Transaction status polling -1. **G1 + G2** — small, standalone gateway PR (rebased after the Orca SDK branch merges). -2. **E1 + H1 + H3** — one hummingbot PR: makes closes retry-until-closed for every CLMM connector **and both topologies**, and fixes the false-COMPLETE hazard. -3. **A1 + A2** — hummingbot-api PR: orphan flagging on completion, DB-aware stop (kills the 404 symptom). -4. **A3 + C1** — condor provider surfaces failures to the agent; `lp_rebalancer` orphan guard for the bot topology. -5. **A4 + H2** — reconciliation endpoint/routine, backoff, retry-counter observability. +Gateway's `/poll` reports one of four statuses; the connector's 1 s poller acts on them: -Note the leverage ordering: E1 fixes the stranding at the only layer shared by both topologies; A1–A3 exist so that *when* E1 exhausts its retries, the failure is visible and actionable instead of silently orphaning a funded position. - ---- - -## 8. Implementation status (2026-08-12) - -All of the above is implemented as uncommitted working-tree changes: - -| Change | Repo / files | Notes | -|---|---|---| -| **G1** 6018 → `SLIPPAGE_EXCEEDED` | gateway: `solana-error-parser.ts` (+ test) | Ported from `040e99e` | -| **G2** simulation-`err` rejection | gateway: `solana.ts` (+ test) | Only the compute-estimation guard was still missing on `development` — the wallet chokepoint already pre-flights via `simulateWithErrorHandling`; the fix branch was based on an older base. The retry loop was **not** ported, per §6. | -| **E1** bounded close re-entry | hummingbot: `lp_executor.py` `_handle_close_failure` | Stays `CLOSING`, increments `_current_retries` (wires the dead `_max_retries_reached` field); `FAILED` only on exhaustion | -| **H1** operation-aware retry codes | hummingbot: `gateway_base.py` `_execute_with_retry`/`_classify_error`, `gateway.py` `_clmm_close_position` | Close opts in to `SLIPPAGE_EXCEEDED` + `SIMULATION_FAILED` | -| **H2** backoff + observability | hummingbot: `gateway_base.py` (1 s between attempts), `lp_executor.py` `get_custom_info` (`current_retries`/`max_retries`/`max_retries_reached`) | | -| **H3** `get_position_info` contract | hummingbot: `gateway.py` | `None` = definitive 404 only; transient errors re-raise. `_update_position_info` treats 3 consecutive definitive misses as an external close; post-create fetch guarded so a blip can't route a fresh position to `CLOSING` | -| **C1** orphan guard | hummingbot: `controllers/generic/lp_rebalancer` + mirrored in hummingbot-api `bots/controllers/...` | Halts re-creation when the FAILED executor still reports a `position_address` | -| **A1** orphan flag | hummingbot-api: `executor_service.py` | `orphaned_position: true` injected into persisted `final_state`; error-level log on completion | -| **A2** DB-aware stop | hummingbot-api: `executor_service.py`, `models/executors.py` | Terminal executor → `status="already_terminated"` no-op with `close_type`/`position_address`/`orphaned_position`; 404 only for unknown ids | -| **A4** orphan listing | hummingbot-api: `GET /executors/positions/orphaned` (router + service + repository) | DB-side: FAILED-with-position + `SYSTEM_CLEANUP` LP restarts (flagged `needs_onchain_reconciliation`) | -| **A3** agent visibility | condor: `agents/providers/executors.py`, `guides/lp_executor.md` | Orphaned executors surface in the tick-prompt narrative (`🚨 ORPHANED POSITION`) and in provider data; guide updated for the new stop/orphan contract | - -Validation: gateway — targeted jest (63 passed), `tsc` and eslint clean; hummingbot — `test/hummingbot/connector/gateway/` + lp_executor suites (184 passed, incl. new contract tests), flake8 clean; hummingbot-api — syntax/import checks, pre-existing `test_gateway_lp_executor.py` staleness confirmed unrelated via stash; condor — MCP tools + executor/performance suites pass. - ---- - -## 9. Adversarial review outcomes (2026-08-12) - -Two independent adversarial reviews were run against the implementation. Confirmed findings and their resolutions: - -| Finding | Severity | Resolution | +| `txStatus` | Meaning | Connector behavior | |---|---|---| -| `@async_ttl_cache` stores `None`, so one cached 404 satisfied the "3 consecutive misses" guard in 3 ticks; combined with a lagging post-create read, a **live fresh position could be falsely COMPLETEd and the controller would stack a second one** | Critical | `get_position_info_fresh` (uncached) added; all existence decisions (close pre-flight, monitoring, post-create) use it. Guard additionally gated on the position having been seen on-chain at least once. Both behaviors pinned by tests | -| "None = definitive 404" was substring-matched on "not found" — the HTTP client stamps "(Not Found)" on **every** 404, so a missing route (Gateway redeploy) or missing wallet file read as "position gone" | Critical | Match narrowed to position-specific messages (verified against actual Gateway route strings); contract pinned by a dedicated test file | -| Retry stacking: executor (11) × connector (11) = up to 121 build/submit cycles, ~1.9 h per close on timeouts — worse than the 10×3 stacking §3 condemned | Major | Connector inner budget for close cut to `min(3, max_retries)`; executor owns the policy | -| Transport errors (connection refused/5xx, no `[code:]`) fail instantly → all executor retries burned in ~11 s during a 20 s Gateway restart → permanent FAILED + halt | Major | Exponential backoff (2ⁿ s capped at 30 s) between executor close attempts; a Gateway restart now spans a few retries instead of all of them | -| Landed-but-failed close (`status: -1`) raised with no error code → unretryable even with the H1 opt-in — the flagship #678 shape missed the fast path | Major | Raise now typed `[code: TX_NOT_CONFIRMED]`; close opts in | -| Agent-facing recovery text said a "fresh lp_executor" could recover an orphan — it cannot (configs have no adoption field); following it would mint a second position | High | Wording corrected everywhere (provider, guide, MCP outputs): gateway-side close by position address, then `resolve_orphan` | -| `GET /executors/positions/orphaned` was unreachable from the MCP tool it was documented for | High | `manage_executors` gained `orphaned` and `resolve_orphan` actions (raw-HTTP passthrough, same pattern as `get_logs`) | -| Orphan listing applied `limit` to mixed candidates then filtered in Python — restarts/failed swaps could push real orphans past the newest 500 and return a false "no orphans" | Med-High | `executor_type='lp_executor'` filter moved into SQL (the only type owning position accounts); DB errors now raise instead of returning `[]` | -| No way to ever silence a recovered orphan — 🚨 warnings forever | Medium | `POST /executors/{id}/resolve-orphan` sets `orphan_resolved` in the final state; listing, provider, and warnings all skip resolved records | -| "404 = never existed" was overclaimed: completion race, failed persist, or DB blip still 404'd a real executor | Medium | Stop now treats any DB-known, not-in-memory executor as `already_terminated` regardless of stored status (reconciling stale RUNNING rows); docs soften the 404 meaning | -| Telegram handler and MCP stop output showed "✅ stopped" for a no-op stop, hiding the orphan payload | Medium | Both presenters branch on `already_terminated` and surface the orphan warning | - -### Live smoke test (2026-08-12) - -The hummingbot wheel was rebuilt (`hummingbot-20260729-cp312`, macOS arm64) and force-reinstalled into the `hummingbot-api` conda env; import inspection confirmed the installed package carries every change (fresh reads, opt-in codes, `TX_NOT_CONFIRMED`, E1 backoff, seen-gate). The patched gateway ran in dev mode on :15889 (the production container on :15888 untouched), and the **H3 contract was verified end-to-end through the installed wheel against the live gateway** — all read-only, no on-chain writes: - -1. Nonexistent position on a live Orca route → connector returns `None` (`"Position not found or closed: …"` 404) ✅ -2. Nonexistent connector route (the HTTP client stamps `(Not Found)` on it) → connector **raises** instead of `None` ✅ -3. Dead gateway port → connector raises `ClientConnectorError` ✅ - -The smoke test also caught two live gateway bugs, both fixed and re-verified: - -- **Close-position on a missing position returned a generic 500**, not the 404 callers reconcile on: the legacy whirlpool SDK's `getPosition` *throws* (`"Unable to fetch Position at address…"`) instead of returning null, so the route's not-found branch was dead code. The close route now maps that throw to `404 Position not found`. -- **`Orca.getPositionInfo` swallowed *every* error into `null`** — meaning a transient RPC failure inside Gateway was served as the definitive `"Position not found or closed"` 404, silently reopening the abandon-live-position hole one layer *below* the H3 connector fix. It now returns null only for the SDK's definitive account-missing throw and rethrows everything else (transient errors surface as 500s with no position-specific text → the connector raises). Both behaviors pinned by tests; 216 gateway tests pass. - -### Docker rebuild + funded live test (2026-08-12) - -Both production containers were rebuilt with the patched code and the full cycle was run with real funds on Orca mainnet: - -- **Images**: a linux-arm64 wheel (`hummingbot-20260729-cp312-linux_aarch64`) was built in a container from the patched source and layered onto `hummingbot/hummingbot-api:latest` together with the updated API source (`Dockerfile.patched-hummingbot`); `hummingbot/gateway:development` was rebuilt from the patched checkout. Both containers were recreated with identical wiring (conf/cert volumes, `emqx-bridge` network, 127.0.0.1 port binds); in-container inspection confirmed the patched code, and the new routes appeared in the live OpenAPI spec. -- **Funded cycle** (wallet `82Sgg…qyHx5`, Topology B via `POST /executors/`): opened a RANGE lp_executor on the Orca SOL-USDC 0.04% whirlpool (`Czfq…44zE`) with 0.04 SOL + 3 USDC → position `6hep…Hkup` live on-chain, `IN_RANGE`, amounts verified independently through the gateway. Stopped with `keep_position=true` → `CLOSING` → closed on-chain first-attempt (`current_retries: 0/10` visible in `custom_info` — H2 observability live), `POSITION_HOLD/COMPLETE`, full rent refunded (0.01006 SOL), fees earned recorded, total tx cost ~0.00004 SOL. -- **Post-close verification**: gateway position-info returns the position-specific 404 for the closed position; a second stop returned `status: "already_terminated"` with final `close_type` — **the exact #678 dead-end (stop → 404) is gone in production**; `/executors/positions/orphaned` correctly empty. -- **Bonus real-world failure exercised**: the first create attempt failed on a public-RPC rate limit — the executor went `FAILED` with **no** position address, was correctly **not** flagged as an orphan (nothing landed on-chain, no funds spent), and the fix was a conf change (QuickNode RPC) + retry. Exactly the intended failure semantics for the open path. - -### Close-failure stress test (2026-08-12) - -**Natural stress: 5× narrow open→close cycles.** Five positions opened at ±0.07% around the live price on the SOL-USDC 0.04% whirlpool and closed immediately — all five closed first-attempt (`close_retries: 0`), full rent refunded, zero orphans. The natural 6018 race is effectively unreachable on this branch because the legacy-SDK close route quotes withdrawal minimums with a **50% slippage buffer** (the fix branch's SDK rework quotes at the configured `slippagePct` — which is what made #678 possible at ~1%). +| `1` CONFIRMED | Landed without error | Fill the order | +| `0` PENDING | The chain has seen the transaction | Keep polling — it can still confirm (unbounded by design) | +| `-1` FAILED | Landed with an error | Fail the order, trigger `TransactionFailure` | +| `-2` NOT_FOUND | Unknown to the chain — never received or dropped | Transient while the order is younger than 120 s (blockhash validity margin); afterwards each miss counts toward the tracker's lost-order limit → order fails after repeated consecutive misses | -**Forced failure: fault-injected gateway image.** To exercise the full failure machinery deterministically, a throwaway `gateway:close-stress-test` image set the close instruction's token minimums to 2×actual+1 — guaranteeing Whirlpool `6018 TokenMinSubceeded` on every attempt. One funded position (`9xiY…D4tW`) was opened and stopped against it. Observed, end to end on production containers: - -1. **G2 made every failed attempt free**: each of the 33 gateway POSTs (11 executor attempts × 3 connector retries) was rejected by the compute-simulation guard *before broadcast* — zero fees burned across the entire cascade. -2. **H1 opt-in retried at the connector**: 3 inner attempts per executor attempt, ~1.8 s apart. -3. **E1 bounded re-entry with visible backoff**: `close_retries` climbed 0→11 over ~5 minutes with the exponential backoff plainly in the timestamps (2, 4, 8, 16, then 30 s capped), each re-entry rebuilding from fresh state. -4. **Terminal + A1**: exhaustion → `FAILED`, `max_retries_reached: true`, `orphaned_position: true` in the persisted state, error-level orphan log pointing at the recovery endpoint. -5. **A4/A2 live**: `/executors/positions/orphaned` listed the record with the position address; re-stop returned `already_terminated` + `orphaned_position: true` + the address — the actionable payload instead of a 404. -6. **Recovery**: gateway restored to the normal image → direct close succeeded first try (all funds + rent recovered) → `resolve-orphan` cleared the listing and the flag. - -**The forced failure also caught a real parser bug** — the same misreporting #678 complained about, in a shape unit tests missed: `extractProgramId` matched the *first* `Program X invoke|failed` in the message, and simulation logs open with prelude programs (ComputeBudget), so the failing DEX program's error table was never consulted and Orca 6018 fell through to the generic map's `MATH_OVERFLOW` / `SIMULATION_FAILED`. The close survived anyway **only because** the opt-in set includes both `SIMULATION_FAILED` and `SLIPPAGE_EXCEEDED` — defense-in-depth proving its worth live. Fixed: the parser now attributes a custom error to the program on the `failed: custom program error` line (regression test with a full simulation-shaped log; 53 parser tests pass), and the rebuilt `gateway:development` image carries the fix. Also fixed from live observation: an off-by-one that ran `max_retries + 1` close attempts while logging `n/max`. The hummingbot-api image was subsequently rebuilt with the corrected wheel and redeployed; a 2-cycle funded open/close retest on the same pool passed clean (0 retries, rent refunded, 0 orphans), so the deployed stack is fully current with source. - -Accepted residual risks (documented, not fixed): -- **C1 halt is process-local** — a bot restart clears it and the controller can blind-reopen; the durable guard is the API-layer orphan flag/listing, which is Topology-B infrastructure. A bot-side persistent guard would need controller state persistence that doesn't exist today. -- **Force-stop stragglers** — `force_stop_with_position_hold` doesn't cancel an in-flight `_clmm_close_position`; a lingering attempt can land *after* the executor is persisted FAILED-with-position, making the orphan flag stale-wrong. `resolve_orphan` (after an on-chain check) is the correction path. -- **Deployment coupling** — hummingbot-api runs the *installed* hummingbot wheel; E1/H1/H3 reach Topology B only after the wheel is rebuilt and reinstalled (see `~/.claude/CLAUDE.md` build steps). Until then the API-side changes are live but the executor still hard-fails closes. -- **SYSTEM_CLEANUP orphans have no position address** (final state was never persisted) — they are listed as `needs_onchain_reconciliation` rather than resolved automatically. +Transient poll errors (RPC failures) report `PENDING`, never `NOT_FOUND` — an unknown outcome is a reason to poll again, not to give up. Note the LP flow's writes do not depend on this poller to progress: the connector's operation calls only return a signature after in-request confirmation. The bound matters for flows that record a hash at submission time, and for the RPC cost of stuck orders. --- -## 10. Redesign: exhausted close ends as an involuntary POSITION_HOLD (2026-08-12) - -§7's terminal state — `FAILED` with the position flagged as an orphan — solved #678 but required a parallel side-channel (the `orphaned_position` flag, the orphan listing, the C1 halt, condor prompt injection) at every layer. Surveying how the rest of the executor family handles failure shows why: **we picked the wrong terminal close type**, and every consumer then needed bespoke plumbing to compensate. This section supersedes the terminal-state design in §7 (E1's *retry loop* is unchanged — it matched the family all along). - -### What the executor family already does - -- **In-executor bounded retry is the standard, not an LP invention.** Order, grid, DCA and TWAP executors all increment `_current_retries` on failure and stay in state; the base class's `control_loop` calls `evaluate_max_retries()` after every `control_task`, and that hook terminates the executor when `_current_retries > _max_retries` (`executor_base.py:254-262`). E1 re-implemented exactly this shape inline. -- **`FAILED` means "abnormal end with *no residual exposure*".** `force_stop_with_position_hold` (`executor_base.py:241-252`) sets `POSITION_HOLD if held_orders else FAILED`: anything that still represents exposure terminates as a hold with `held_position_orders`; `FAILED` is reserved for executors with nothing left behind. -- **The hold channel has a real consumer on both topologies.** The orchestrator folds `held_position_orders` from `POSITION_HOLD` executors into durable `PositionHold` records (`executor_orchestrator.py:585-640`); hummingbot-api's `ExecutorService` has the same machinery independently, **including recovery from the database on restart** (`executor_service.py:159-224`). Ending close-exhaustion as `FAILED` bypassed all of it. - -### The constraint: a live LP position is not spot exposure - -`PositionHold` is a spot net-position accumulator (amounts, avg entry, realized PnL). A still-open on-chain position cannot be booked as spot amounts — the tokens sit in the pool, not the wallet — which is exactly why the forced-stop path logs "an on-chain position cannot be represented as spot orders" (`lp_executor.py:986-1005`) instead of inventing amounts. The LP executor already has the answer for "report something without booking anything": the **zero-amount marker order** (`lp_executor.py:904-921`), which `PositionHold._process_order` skips (`amount_base == 0` returns early) while still making `held_position_orders` non-empty and carrying metadata. - -### The design - -1. **`_handle_close_failure` only counts and paces.** It increments `_current_retries`, arms the exponential backoff, and stays `CLOSING`. The terminal decision moves out of the handler and into the family hook. -2. **`evaluate_max_retries` is overridden in `lp_executor`** (previously inert there). Base semantics (`>`, i.e. `max_retries + 1` total attempts — the family convention; this supersedes the `>=` change from §9's stress test) decide *when*; the override decides *how*: - - **Position still on-chain** → append a zero-amount marker order carrying `position_address`, `lp_unclosed_position: true`, and the tx fees burned so far; set `hold_reason: "close_retries_exhausted"`; terminate as **`POSITION_HOLD`**. Accounting is untouched (zero amounts), but the hold — with the position's identity — lands in the durable hold store on both topologies. - - **No position** (close exhausted after the position turned out to be gone, or a failure path with nothing on-chain) → `FAILED`, now true to its family meaning. -3. **Voluntary vs involuntary holds are distinguished by `hold_reason`**, not by close type. A `keep_position=True` stop and an exhausted close both end `POSITION_HOLD`; only the latter carries `hold_reason` (and the marker order instead of a net-trade order). -4. **Consumers key off the reason, not `FAILED`:** - - `lp_rebalancer`: the halt-and-log guard (C1) now triggers on an involuntary hold (`hold_reason` set) as well as legacy `FAILED`-with-position; the accounting branch must **also skip** involuntary holds — their `base_amount`/`quote_amount` are pool balances of a still-open position, and booking them as returned tokens would corrupt `position_hold` tracking. - - hummingbot-api: `orphaned_position` flag injection, the orphan listing, and `resolve-orphan` accept the `POSITION_HOLD`+`hold_reason` shape (legacy `FAILED`-with-position and `SYSTEM_CLEANUP` remain as defense for force-stop stragglers and restarts). The orphan listing becomes a *view* over involuntary holds rather than a parallel mechanism. - - condor: the provider's orphan filter already keys off the injected flag, so it picks up the new shape automatically; guide wording updates from "FAILED with position" to "involuntarily held". - -### What this closes - -- **The C1 restart residual shrinks**: the involuntary hold is persisted in the hold store (DB-recovered on the API path) rather than living only in a controller field. -- **Family semantics restored**: `FAILED` again implies nothing-left-on-chain; dashboards and PnL reports that special-case `POSITION_HOLD` see the exposure through the same lens as every other executor's residue. -- **One mental model for users**: condor users already understand `POSITION_HOLD` from `keep_position=True` stops; an exhausted close is the same object with a reason attached, not a new 🚨 concept. - -### Implementation status (2026-08-12, uncommitted) - -| Change | Repo / files | -|---|---| -| `_handle_close_failure` counts + paces only; `evaluate_max_retries` override terminates (involuntary `POSITION_HOLD` + marker order with live position, `FAILED` without); `_hold_reason` in `get_custom_info`; family `>` semantics restored | hummingbot: `lp_executor.py` (+ contract tests, 140 pass) | -| Accounting skip + halt guard keyed on `hold_reason` as well as legacy `FAILED`-with-position; closed-bounds capture excludes involuntary holds | hummingbot `controllers/generic/lp_rebalancer` + hapi `bots/controllers/...` mirror | -| Orphan flag injection, listing, and resolve accept the `POSITION_HOLD`+`hold_reason` shape; `hold_reason` in stop/orphan payloads and models; router docs | hummingbot-api: `executor_service.py`, `models/executors.py`, `routers/executors.py` | -| Provider orphan filter adds the `hold_reason` shape; prompt/guide wording updated from "terminated FAILED" to reason-aware | condor: `agents/providers/executors.py`, `guides/lp_executor.md` (146 tests pass) | - -Deliberately unchanged: `force_stop_with_position_hold` still ends a live-position forced stop as `FAILED` (the close may still be in flight at the shutdown deadline; the legacy `FAILED`-with-position orphan class covers it), and the zero-amount marker is not emitted there. The deployed hapi image predates this redesign until the next wheel/image rebuild. - -### Live validation (2026-08-12, rebuilt stack) - -Wheel → image → funded-retest pipeline re-run with the §10 redesign deployed (linux wheel rebuilt in-container, hapi image relayered, container recreated; in-container checks confirmed the involuntary-hold code and `hold_reason` plumbing). - -**Clean cycles (normal gateway image):** two funded open/close cycles on the SOL-USDC whirlpool closed first-attempt (`POSITION_HOLD`/`COMPLETE`, 0 retries, rent refunded, orphan listing empty). A bonus negative test happened naturally: two *opens* were rejected at simulation with Orca `0x177c` (`TokenMaxExceeded` — fast-moving market vs a ±0.07% range) and terminated `FAILED` **with nothing on-chain and no orphan record** — `FAILED`'s restored family meaning, observed live at zero fee cost. - -**Forced cascade (fault-injected image, minimums 2×actual+1):** position `ApT4…Qk8A`, executor `C5y3…sTrG`: -- 11 close attempts (`max_retries + 1`, family `>` semantics) over ~5 min with the 2/4/8/16/30 s backoff visible in the timestamps; every attempt rejected pre-broadcast (zero fees). -- Terminal: `close_type: POSITION_HOLD`, `hold_reason: close_retries_exhausted`, `max_retries_reached: true`, and the zero-amount marker order in `held_position_orders` carrying the position address (`lp_unclosed_position: true`). -- `orphaned_position: true` injected; `/positions/orphaned` listed the `POSITION_HOLD` record with `hold_reason`; re-stop returned `already_terminated` + reason. -- Recovery: normal image restored → direct close first-attempt (0.0174 SOL + 1.448 USDC + fees + 0.0100572 rent recovered) → `resolve-orphan` accepted the `POSITION_HOLD` candidate and cleared the listing. Fault image deleted. - -### Rebase note: Orca SDK migration (gateway#676) - -The Orca connector's migration to the current whirlpools SDK merged into `development` while this work was in flight, which changed the gateway-side surface: +## 4. Accepted residual risks -- The new close route quotes at `slippageToleranceBps` from the connector config (~1%) instead of the legacy route's 50% buffer — the exact condition under which #678 was reachable. The chain-level guards (G1/G2) and the Hummingbot-side retry ownership are therefore **load-bearing** on the new SDK, not defense-in-depth. -- The legacy-SDK `closePosition` 404 mapping is obsolete: the new route uses `fetchMaybePosition` and 404s a missing position natively. -- The `getPositionInfo` null-contract fix (H3) was **ported**: the migrated implementation again swallowed every error to `null`, so it now uses `fetchMaybePosition` for a definitive "account does not exist" → `null`, and lets everything else throw. +- **The controller halt is process-local.** A bot restart clears `_orphaned_position_address` and the rebalancer can reopen while the orphan is unresolved; conversely, resolving the orphan does not un-halt a running controller (restart is the acknowledgment step). Controllers have no persisted state to rebuild the latch from; the durable guard is the API-layer orphan record, listing, and agent warnings. +- **Force-stop stragglers.** A force-stop does not cancel an in-flight close; a lingering attempt can land after the executor is persisted with the orphan shape, making the record stale-wrong. `resolve-orphan` (after an on-chain check) is the correction path. +- **Deployment coupling.** hummingbot-api runs the *installed* hummingbot wheel — the executor- and connector-level fixes reach topology B only after the wheel is rebuilt and reinstalled. +- **`SYSTEM_CLEANUP` orphans have no position address** (no final state was persisted); they are listed for on-chain reconciliation rather than resolved automatically. +- **A lost-order verdict can race a very late confirmation.** An order failed via the NOT_FOUND deadline could in principle confirm afterwards if an RPC node was more than two minutes behind the cluster; the deadline plus the consecutive-miss requirement makes this require a broken RPC, and the alternative — waiting forever — is strictly worse. From 71537de2789e2032481285392a6d05d307d7701f Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 08:18:29 -0700 Subject: [PATCH 04/31] fix(poll): one txStatus contract for Solana and Ethereum; report EVM reverts as FAILED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/chains/ethereum/routes/poll.ts | 63 +++++------------- src/chains/solana/routes/poll.ts | 13 ++-- src/chains/solana/solana.ts | 25 +++---- src/schemas/chain-schema.ts | 10 ++- test/chains/ethereum/routes/poll.test.ts | 83 ++++++++++++++++++++++++ test/chains/solana/routes/poll.test.ts | 29 +++++---- 6 files changed, 143 insertions(+), 80 deletions(-) create mode 100644 test/chains/ethereum/routes/poll.test.ts diff --git a/src/chains/ethereum/routes/poll.ts b/src/chains/ethereum/routes/poll.ts index 3d90df9e06..adf9a77803 100644 --- a/src/chains/ethereum/routes/poll.ts +++ b/src/chains/ethereum/routes/poll.ts @@ -1,7 +1,12 @@ import { ethers } from 'ethers'; import { FastifyPluginAsync, FastifyInstance } from 'fastify'; -import { PollRequestType, PollResponseType, PollResponseSchema } from '../../../schemas/chain-schema'; +import { + PollRequestType, + PollResponseType, + PollResponseSchema, + TransactionStatusCode, +} from '../../../schemas/chain-schema'; import { getConnector } from '../../../services/connection-manager'; import { logger } from '../../../services/logger'; import { Ethereum } from '../ethereum'; @@ -36,57 +41,25 @@ export async function pollEthereumTransaction( const ethereum = await Ethereum.getInstance(network); const currentBlock = await ethereum.getCurrentBlockNumber(); - let txData = await ethereum.getTransaction(signature); + const txData = await ethereum.getTransaction(signature); let txBlock, txReceipt, txStatus; if (!txData) { - const MAX_RETRIES = 3; - const RETRY_DELAY_MS = 1000; - let retryCount = 0; - - while (retryCount < MAX_RETRIES) { - await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS)); - txData = await ethereum.getTransaction(signature); - if (txData) break; - retryCount++; - } - - if (!txData) { - // tx not found after retries - logger.info(`Transaction ${signature} not found in mempool or does not exist after ${MAX_RETRIES} retries.`); - txBlock = -1; - txReceipt = null; - txStatus = -1; - } - } - - if (txData) { + // Unknown to the node: never received or dropped. eth_getTransactionByHash + // returns mempool transactions, so not-found is distinct from pending. + logger.info(`Transaction ${signature} not found in mempool or on-chain.`); + txBlock = -1; + txReceipt = null; + txStatus = TransactionStatusCode.NOT_FOUND; + } else { txReceipt = await ethereum.getTransactionReceipt(signature); if (txReceipt === null) { - // tx is in the mempool + // In the mempool, awaiting inclusion txBlock = -1; - txReceipt = null; - - // In stateless approach, we simply check if the transaction is still pending - // We use a basic status code of 0 for pending transactions in mempool - txStatus = 0; - - // Check if transaction is likely to be processed based on gas price - if (txData.gasPrice) { - const currentGasPrice = await ethereum.estimateGasPrice(); - // Convert current gas price from GWEI to wei for comparison - const currentGasPriceWei = currentGasPrice * 1e9; - // If the transaction's gas price is significantly lower than current gas price, - // it might be stuck (status 3), otherwise it's likely to be processed (status 2) - if (txData.gasPrice.toNumber() < currentGasPriceWei * 0.8) { - txStatus = 3; // Likely stuck - } else { - txStatus = 2; // Likely to be processed - } - } + txStatus = TransactionStatusCode.PENDING; } else { - // tx has been processed txBlock = txReceipt.blockNumber; - txStatus = typeof txReceipt.status === 'number' ? 1 : -1; + // Receipt status 0 = reverted, 1 = success (undefined only pre-Byzantium) + txStatus = txReceipt.status === 0 ? TransactionStatusCode.FAILED : TransactionStatusCode.CONFIRMED; // decode logs if (connector) { diff --git a/src/chains/solana/routes/poll.ts b/src/chains/solana/routes/poll.ts index 6bc584294f..dc55498f29 100644 --- a/src/chains/solana/routes/poll.ts +++ b/src/chains/solana/routes/poll.ts @@ -1,9 +1,14 @@ import { FastifyPluginAsync, FastifyInstance } from 'fastify'; -import { PollRequestType, PollResponseType, PollResponseSchema } from '../../../schemas/chain-schema'; +import { + PollRequestType, + PollResponseType, + PollResponseSchema, + TransactionStatusCode, +} from '../../../schemas/chain-schema'; import { logger } from '../../../services/logger'; import { SolanaPollRequest } from '../schemas'; -import { Solana, TransactionResponseStatusCode } from '../solana'; +import { Solana } from '../solana'; import { parseSolanaError } from '../solana-error-parser'; export async function pollSolanaTransaction( @@ -24,7 +29,7 @@ export async function pollSolanaTransaction( currentBlock, signature, txBlock: null, - txStatus: TransactionResponseStatusCode.NOT_FOUND, + txStatus: TransactionStatusCode.NOT_FOUND, fee: null, error: 'INVALID_INPUT: Invalid transaction signature format', txData: null, @@ -83,7 +88,7 @@ export async function pollSolanaTransaction( currentBlock: await solana.getCurrentBlockNumber(), signature, txBlock: null, - txStatus: TransactionResponseStatusCode.UNCONFIRMED, + txStatus: TransactionStatusCode.PENDING, fee: null, error: `Error polling transaction: ${(err as Error).message}`, txData: null, diff --git a/src/chains/solana/solana.ts b/src/chains/solana/solana.ts index 7e0131995a..6001dcc7d9 100644 --- a/src/chains/solana/solana.ts +++ b/src/chains/solana/solana.ts @@ -43,6 +43,7 @@ import { ChainstackService } from '../../rpc/chainstack-service'; import { HeliusService } from '../../rpc/helius-service'; import { createRateLimitAwareSolanaConnection } from '../../rpc/rpc-connection-interceptor'; import { RPCProvider } from '../../rpc/rpc-provider-base'; +import { TransactionStatusCode } from '../../schemas/chain-schema'; import { ConfigManagerCertPassphrase } from '../../services/config-manager-cert-passphrase'; import { ConfigManagerV2 } from '../../services/config-manager-v2'; import { httpErrors, HttpError } from '../../services/error-handler'; @@ -68,13 +69,6 @@ interface TokenAccount { value: any; } -export enum TransactionResponseStatusCode { - NOT_FOUND = -2, - FAILED = -1, - UNCONFIRMED = 0, - CONFIRMED = 1, -} - export class Solana { public connection: Connection; public network: string; @@ -1177,16 +1171,15 @@ export class Solana { }); } - // returns a Solana TransactionResponseStatusCode for a txData. - public async getTransactionStatusCode(txData: TransactionResponse | null): Promise { + // returns a Solana TransactionStatusCode for a txData. + public async getTransactionStatusCode(txData: TransactionResponse | null): Promise { let txStatus; if (!txData) { // tx not yet confirmed by validator - txStatus = TransactionResponseStatusCode.UNCONFIRMED; + txStatus = TransactionStatusCode.PENDING; } else { // If txData exists, check if there's an error in the metadata - txStatus = - txData.meta?.err == null ? TransactionResponseStatusCode.CONFIRMED : TransactionResponseStatusCode.FAILED; + txStatus = txData.meta?.err == null ? TransactionStatusCode.CONFIRMED : TransactionStatusCode.FAILED; } return txStatus; } @@ -1196,20 +1189,20 @@ export class Solana { // confirmation and a tx that was dropped, so pollers cannot tell them apart from // txData alone. A signature that stays NOT_FOUND after its blockhash expires // (~90s) can never land. - public async getSignatureStatus(signature: string): Promise { + public async getSignatureStatus(signature: string): Promise { const { value } = await this.connection.getSignatureStatuses([signature], { searchTransactionHistory: true, }); const status = value[0]; if (!status) { - return TransactionResponseStatusCode.NOT_FOUND; + return TransactionStatusCode.NOT_FOUND; } if (status.err) { - return TransactionResponseStatusCode.FAILED; + return TransactionStatusCode.FAILED; } // Seen by the cluster: 'processed', or confirmed/finalized racing ahead of // getTransaction visibility — report unconfirmed and let the next poll resolve it. - return TransactionResponseStatusCode.UNCONFIRMED; + return TransactionStatusCode.PENDING; } // returns the current block number diff --git a/src/schemas/chain-schema.ts b/src/schemas/chain-schema.ts index dca9a10245..b74e240bd0 100644 --- a/src/schemas/chain-schema.ts +++ b/src/schemas/chain-schema.ts @@ -94,6 +94,14 @@ export const PollRequestSchema = Type.Object( ); export type PollRequestType = Static; +// Values reported in PollResponse.txStatus, shared by all chains. +export enum TransactionStatusCode { + NOT_FOUND = -2, // unknown to the chain: never received or dropped + FAILED = -1, // landed with an error / reverted + PENDING = 0, // seen by the chain, awaiting confirmation + CONFIRMED = 1, // landed without error +} + export const PollResponseSchema = Type.Object( { currentBlock: Type.Number(), @@ -101,7 +109,7 @@ export const PollResponseSchema = Type.Object( txBlock: Type.Union([Type.Number(), Type.Null()]), txStatus: Type.Number({ description: - 'Transaction status: 1 = confirmed, 0 = pending, -1 = failed, -2 = not found (unknown to the chain; on Solana this is terminal once the transaction blockhash expires)', + 'Transaction status: 1 = confirmed, 0 = pending, -1 = failed, -2 = not found (unknown to the chain: never received or dropped; on Solana this is terminal once the transaction blockhash expires)', }), fee: Type.Union([Type.Number(), Type.Null()]), error: Type.Union([Type.String({ description: 'Error info if failed: "TYPE (code): message"' }), Type.Null()]), diff --git a/test/chains/ethereum/routes/poll.test.ts b/test/chains/ethereum/routes/poll.test.ts new file mode 100644 index 0000000000..fb0b89b32a --- /dev/null +++ b/test/chains/ethereum/routes/poll.test.ts @@ -0,0 +1,83 @@ +// Import shared mocks before importing app +import '../../../mocks/app-mocks'; + +import { Ethereum } from '../../../../src/chains/ethereum/ethereum'; +import { pollEthereumTransaction } from '../../../../src/chains/ethereum/routes/poll'; +import { TransactionStatusCode } from '../../../../src/schemas/chain-schema'; + +// Mock the Ethereum class +jest.mock('../../../../src/chains/ethereum/ethereum', () => ({ + Ethereum: { + getInstance: jest.fn(), + }, +})); + +const mockEthereum = Ethereum as jest.Mocked; + +const TX_HASH = '0x' + 'ab'.repeat(32); + +describe('pollEthereumTransaction', () => { + const mockEthereumInstance = { + getCurrentBlockNumber: jest.fn(), + getTransaction: jest.fn(), + getTransactionReceipt: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockEthereum.getInstance.mockResolvedValue(mockEthereumInstance as any); + mockEthereumInstance.getCurrentBlockNumber.mockResolvedValue(21000000); + }); + + it('returns NOT_FOUND when the node does not know the transaction', async () => { + mockEthereumInstance.getTransaction.mockResolvedValue(null); + + const result = await pollEthereumTransaction(null as any, 'mainnet', TX_HASH); + + expect(result.txStatus).toBe(TransactionStatusCode.NOT_FOUND); + expect(mockEthereumInstance.getTransactionReceipt).not.toHaveBeenCalled(); + }); + + it('returns PENDING while the transaction sits in the mempool', async () => { + mockEthereumInstance.getTransaction.mockResolvedValue({ + hash: TX_HASH, + gasLimit: { toString: () => '21000' }, + value: { toString: () => '0' }, + }); + mockEthereumInstance.getTransactionReceipt.mockResolvedValue(null); + + const result = await pollEthereumTransaction(null as any, 'mainnet', TX_HASH); + + expect(result.txStatus).toBe(TransactionStatusCode.PENDING); + expect(result.txBlock).toBe(-1); + }); + + it('returns CONFIRMED for a mined transaction with receipt status 1', async () => { + mockEthereumInstance.getTransaction.mockResolvedValue({ + hash: TX_HASH, + gasLimit: { toString: () => '21000' }, + value: { toString: () => '0' }, + }); + mockEthereumInstance.getTransactionReceipt.mockResolvedValue({ status: 1, blockNumber: 20999999, logs: [] }); + + const result = await pollEthereumTransaction(null as any, 'mainnet', TX_HASH); + + expect(result.txStatus).toBe(TransactionStatusCode.CONFIRMED); + expect(result.txBlock).toBe(20999999); + }); + + it('returns FAILED for a reverted transaction (receipt status 0)', async () => { + // Regression: receipt.status 0 is a number, so the old + // `typeof status === 'number' ? 1 : -1` mapping reported reverts as confirmed. + mockEthereumInstance.getTransaction.mockResolvedValue({ + hash: TX_HASH, + gasLimit: { toString: () => '21000' }, + value: { toString: () => '0' }, + }); + mockEthereumInstance.getTransactionReceipt.mockResolvedValue({ status: 0, blockNumber: 20999999, logs: [] }); + + const result = await pollEthereumTransaction(null as any, 'mainnet', TX_HASH); + + expect(result.txStatus).toBe(TransactionStatusCode.FAILED); + }); +}); diff --git a/test/chains/solana/routes/poll.test.ts b/test/chains/solana/routes/poll.test.ts index 50440b4b86..6e123e6eb0 100644 --- a/test/chains/solana/routes/poll.test.ts +++ b/test/chains/solana/routes/poll.test.ts @@ -2,7 +2,8 @@ import '../../../mocks/app-mocks'; import { pollSolanaTransaction } from '../../../../src/chains/solana/routes/poll'; -import { Solana, TransactionResponseStatusCode } from '../../../../src/chains/solana/solana'; +import { Solana } from '../../../../src/chains/solana/solana'; +import { TransactionStatusCode } from '../../../../src/schemas/chain-schema'; // Mock the Solana class jest.mock('../../../../src/chains/solana/solana', () => ({ @@ -36,11 +37,11 @@ describe('pollSolanaTransaction', () => { slot: 365794000, meta: { fee: 5000, err: null }, }); - mockSolanaInstance.getTransactionStatusCode.mockResolvedValue(TransactionResponseStatusCode.CONFIRMED); + mockSolanaInstance.getTransactionStatusCode.mockResolvedValue(TransactionStatusCode.CONFIRMED); const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); - expect(result.txStatus).toBe(TransactionResponseStatusCode.CONFIRMED); + expect(result.txStatus).toBe(TransactionStatusCode.CONFIRMED); expect(result.txBlock).toBe(365794000); expect(result.fee).toBe(5000 / 1e9); expect(result.error).toBeNull(); @@ -52,37 +53,37 @@ describe('pollSolanaTransaction', () => { }); it('returns UNCONFIRMED when the cluster has seen the signature', async () => { - mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionResponseStatusCode.UNCONFIRMED); + mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionStatusCode.PENDING); const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); expect(mockSolanaInstance.getSignatureStatus).toHaveBeenCalledWith(VALID_SIGNATURE); - expect(result.txStatus).toBe(TransactionResponseStatusCode.UNCONFIRMED); + expect(result.txStatus).toBe(TransactionStatusCode.PENDING); expect(result.error).toBeNull(); }); it('returns NOT_FOUND when the signature is unknown to the cluster', async () => { - mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionResponseStatusCode.NOT_FOUND); + mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionStatusCode.NOT_FOUND); const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); - expect(result.txStatus).toBe(TransactionResponseStatusCode.NOT_FOUND); + expect(result.txStatus).toBe(TransactionStatusCode.NOT_FOUND); expect(result.error).toBeNull(); }); it('returns FAILED when the signature status carries an error', async () => { - mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionResponseStatusCode.FAILED); + mockSolanaInstance.getSignatureStatus.mockResolvedValue(TransactionStatusCode.FAILED); const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); - expect(result.txStatus).toBe(TransactionResponseStatusCode.FAILED); + expect(result.txStatus).toBe(TransactionStatusCode.FAILED); }); }); it('returns NOT_FOUND for a malformed signature, which can never resolve', async () => { const result = await pollSolanaTransaction(null as any, 'mainnet-beta', 'not-a-signature'); - expect(result.txStatus).toBe(TransactionResponseStatusCode.NOT_FOUND); + expect(result.txStatus).toBe(TransactionStatusCode.NOT_FOUND); expect(result.error).toContain('INVALID_INPUT'); expect(mockSolanaInstance.getTransaction).not.toHaveBeenCalled(); }); @@ -92,7 +93,7 @@ describe('pollSolanaTransaction', () => { const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); - expect(result.txStatus).toBe(TransactionResponseStatusCode.UNCONFIRMED); + expect(result.txStatus).toBe(TransactionStatusCode.PENDING); expect(result.error).toContain('RPC unavailable'); }); }); @@ -110,7 +111,7 @@ describe('Solana.getSignatureStatus', () => { it('maps a null status to NOT_FOUND', async () => { const instance = makeInstance(null); - await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionResponseStatusCode.NOT_FOUND); + await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionStatusCode.NOT_FOUND); expect(instance.connection.getSignatureStatuses).toHaveBeenCalledWith([VALID_SIGNATURE], { searchTransactionHistory: true, }); @@ -121,11 +122,11 @@ describe('Solana.getSignatureStatus', () => { err: { InstructionError: [2, { Custom: 6018 }] }, confirmationStatus: 'confirmed', }); - await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionResponseStatusCode.FAILED); + await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionStatusCode.FAILED); }); it('maps a processed (seen but unconfirmed) status to UNCONFIRMED', async () => { const instance = makeInstance({ err: null, confirmationStatus: 'processed' }); - await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionResponseStatusCode.UNCONFIRMED); + await expect(instance.getSignatureStatus(VALID_SIGNATURE)).resolves.toBe(TransactionStatusCode.PENDING); }); }); From 18d6e95ed4f9d18ff84c5f78791fdd460064da6e Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 08:24:25 -0700 Subject: [PATCH 05/31] docs: single close-retry loop; unified poll contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/retry-architecture.md | 46 ++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/docs/retry-architecture.md b/docs/retry-architecture.md index 4c24759dc2..456cea1a57 100644 --- a/docs/retry-architecture.md +++ b/docs/retry-architecture.md @@ -11,8 +11,8 @@ | Layer | Question it answers | What it owns | |---|---|---| | **Gateway** | "Did *this one attempt* land?" | Reads and transport only: RPC 429s, confirmation polling until blockhash expiry, typed error classification, pre-broadcast simulation rejection. Never re-submits a write. | -| **Connector** (`gateway_base.py`, `gateway.py`) | "Should I ask Gateway for *another attempt*?" | `_execute_with_retry`: re-POSTs the operation (Gateway rebuilds fresh each time). Timeouts retry for every operation; additional error codes are an explicit, per-operation opt-in. | -| **LP executor** (`lp_executor.py`) | "What does this failure mean for *the position lifecycle*?" | State-machine re-entry with a bounded budget and exponential backoff; reconciliation of already-succeeded outcomes; the terminal close type. | +| **Connector** (`gateway_base.py`, `gateway.py`) | "Should I ask Gateway for *another attempt*?" | `_execute_with_retry`: transport timeouts only, the same for every operation. What an operation error means is the caller's decision. | +| **LP executor** (`lp_executor.py`) | "What does this failure mean for *the position lifecycle*?" | **The close retry loop** — state-machine re-entry with a bounded budget and exponential backoff, a fresh position read before every re-submit, and the terminal close type. It passes `max_retries=0` so the connector makes exactly one request per re-entry. | | **Controller** (`lp_rebalancer.py`) | "What does this failure mean for *the strategy*?" | Re-creates a fresh executor with freshly computed bounds; halts instead of stacking exposure over an unresolved position. | | **hummingbot-api / condor** | "What survives the process, and who is told?" | Durable orphan records, listing and resolution endpoints, agent-facing warnings. | @@ -20,8 +20,8 @@ flowchart TB subgraph HB["Hummingbot — stateful, owns retries"] C["Controller: lp_rebalancer
strategy: re-create after clean FAILED,
halt over unresolved position
"] - E["LP executor: lp_executor
lifecycle: CLOSING re-entry, max_retries + 1 attempts,
exponential backoff (2ⁿ s, cap 30 s)
"] - K["Connector: gateway_base / gateway
operation: _execute_with_retry —
timeouts always; close opts in to slippage-class errors,
inner budget 3
"] + E["LP executor: lp_executor
lifecycle: CLOSING re-entry — the ONE close retry loop,
max_retries + 1 attempts, exponential backoff (2ⁿ s, cap 30 s),
fresh position read before every re-submit
"] + K["Connector: gateway_base / gateway
operation: _execute_with_retry — transport timeouts only;
close passes max_retries=0: one request per re-entry
"] end subgraph GW["Gateway — stateless, one attempt per request"] R["Route: build tx from FRESH on-chain state
quote → simulate → sign → broadcast"] @@ -52,19 +52,19 @@ The #678 investigation and the adversarial review of the fix surfaced the follow 2. **Doomed transactions were broadcast anyway.** The compute-estimation simulation's `err` was ignored (only `unitsConsumed` was read), so a transaction guaranteed to fail was signed, broadcast, and paid fees. Both send paths now reject a failed simulation with a typed 400 **before broadcast** — a failed attempt costs nothing. 3. **Transient read errors served as definitive closure.** `Orca.getPositionInfo` swallowed every error into `null`, which the route surfaced as the position-specific 404 — so an RPC blip inside Gateway read as "position closed". It now returns `null` only for a definitive account-does-not-exist result and rethrows everything else. 4. **A dropped transaction was indistinguishable from a pending one.** `getTransaction` (commitment `confirmed`) returns null both for a transaction awaiting confirmation and for one the cluster has never seen, and `/poll` reported both as `txStatus 0` (pending) — forever. The poll now consults the signature-status cache (with history search) and reports an unknown signature as **`NOT_FOUND` (-2)**, which is terminal once the transaction's blockhash has expired (~90 s). -5. **Post-SDK-migration slippage exposure.** The migrated Orca close route quotes withdrawal minimums at the configured `slippagePct` (~1%) rather than the legacy 50% buffer — exactly the condition under which the #678 race is reachable. The guards above are load-bearing, not defense-in-depth. +5. **The two poll routes spoke different dialects.** Ethereum's poll used raw numbers including `2` ("likely to be processed") and `3` ("likely stuck") that no consumer understood, reported not-found as `-1` (failed) after blocking the request for three in-route 1-second retries, and — via `typeof receipt.status === 'number' ? 1 : -1` — reported **reverted** transactions (receipt status `0`, which is a number) as CONFIRMED, so a reverted swap polled as filled. Both routes now share one `TransactionStatusCode` contract: `NOT_FOUND (-2) / FAILED (-1) / PENDING (0) / CONFIRMED (1)`. +6. **Post-SDK-migration slippage exposure.** The migrated Orca close route quotes withdrawal minimums at the configured `slippagePct` (~1%) rather than the legacy 50% buffer — exactly the condition under which the #678 race is reachable. The guards above are load-bearing, not defense-in-depth. ### Hummingbot connector -6. **The retry classifier was operation-blind.** `SLIPPAGE_EXCEEDED`/`SIMULATION_FAILED` were globally non-retryable — correct for a swap (re-submitting the same intent re-fails or double-spends), wrong for a close, where every re-POST rebuilds amounts from fresh state and success consumes the position account (idempotent). `_execute_with_retry` now takes per-operation `retryable_error_codes`; close opts in to `{SLIPPAGE_EXCEEDED, SIMULATION_FAILED, TX_NOT_CONFIRMED}`. -7. **The landed-but-failed shape had no error code.** A broadcast transaction that failed on-chain raised with no `[code:]` marker at all, so the flagship #678 failure shape was unretryable even with an opt-in. It now raises typed `TX_NOT_CONFIRMED`. -8. **`get_position_info` swallowed every exception into `None`**, and the executor read `None` as "already closed" → `COMPLETE` — a transient RPC error during close could **abandon a live position while reporting success**. The contract is now: `None` only for a position-specific 404; everything else re-raises. Three coupled requirements: (a) the match is position-specific, because the HTTP client stamps "(Not Found)" on *every* 404 — a missing route after a redeploy read as "position gone"; (b) existence decisions use an **uncached** read (`get_position_info_fresh`) — the 5 s TTL cache stores `None` like any value, so one cached 404 masqueraded as several independent confirmations; (c) external-close detection requires the position to have been **seen on-chain at least once** plus 3 consecutive fresh misses. -9. **Pending-transaction polling was unbounded.** `update_order_status` polled any in-flight order at 1 s forever; combined with issue 4, a dropped transaction never resolved — the order never failed and burned an RPC call per second until restart. The connector now treats `NOT_FOUND` as transient while the order is younger than `TX_NOT_FOUND_DEADLINE` (120 s, past blockhash validity) and afterwards feeds each miss to the order tracker's existing lost-order machinery, which fails the order after repeated consecutive misses. `PENDING` remains unbounded by design: the chain has seen the transaction, so it can still confirm. +7. **The connector silently decided what operation errors meant.** `SLIPPAGE_EXCEEDED`/`SIMULATION_FAILED` were classified inside `_execute_with_retry`, one size for all operations — and any attempt to make that operation-aware (an opt-in error set, a per-operation inner budget) produced two nested retry loops with multiplying budgets. The connector now retries **transport timeouts only**, identically for every operation; what an operation error means is the caller's decision, and for close the caller is the executor's single loop (issue 11). +8. **The landed-but-failed shape had no error code.** A broadcast transaction that failed on-chain raised with no `[code:]` marker at all, so callers could not classify the flagship #678 failure shape. It now raises typed `TX_NOT_CONFIRMED`. +9. **`get_position_info` swallowed every exception into `None`**, and the executor read `None` as "already closed" → `COMPLETE` — a transient RPC error during close could **abandon a live position while reporting success**. The contract is now: `None` only for a position-specific 404; everything else re-raises. Three coupled requirements: (a) the match is position-specific, because the HTTP client stamps "(Not Found)" on *every* 404 — a missing route after a redeploy read as "position gone"; (b) existence decisions use an **uncached** read (`get_position_info_fresh`) — the 5 s TTL cache stores `None` like any value, so one cached 404 masqueraded as several independent confirmations; (c) external-close detection requires the position to have been **seen on-chain at least once** plus 3 consecutive fresh misses. +10. **Pending-transaction polling was unbounded.** `update_order_status` polled any in-flight order at 1 s forever; combined with issue 4, a dropped transaction never resolved — the order never failed and burned an RPC call per second until restart. The connector now treats `NOT_FOUND` as transient while the order is younger than `TX_NOT_FOUND_DEADLINE` (120 s, past blockhash validity) and afterwards feeds each miss to the order tracker's existing lost-order machinery, which fails the order after repeated consecutive misses. `PENDING` remains unbounded by design: the chain has seen the transaction, so it can still confirm. ### LP executor -10. **Close failure was terminal on the first error.** `_handle_close_failure` jumped straight to `FAILED` with the position still open. It now counts the attempt, arms exponential backoff (2ⁿ s, capped 30 s — so a Gateway restart spans a few retries instead of burning the whole budget in seconds), and stays `CLOSING`; the family hook `evaluate_max_retries` decides termination after `max_retries + 1` attempts. -11. **Retry stacking.** With the executor loop added, the connector's default 10 inner retries would stack into ~O(n²) submissions per close. The connector's inner budget for close is `min(3, max_retries)`; the executor owns the policy. +11. **Close failure was terminal on the first error.** `_handle_close_failure` jumped straight to `FAILED` with the position still open. It now counts the attempt, arms exponential backoff (2ⁿ s, capped 30 s — so a Gateway restart spans a few retries instead of burning the whole budget in seconds), and stays `CLOSING`; the family hook `evaluate_max_retries` decides termination after `max_retries + 1` attempts. This re-entry is the **only** close retry loop: the executor passes `max_retries=0` so the connector makes one request per attempt, and budgets cannot multiply. 12. **The wrong terminal close type.** `FAILED` in the executor family means "abnormal end with *no residual exposure*" — everything downstream (hold store, PnL, dashboards) assumes it. Terminating an exhausted close as `FAILED` forced a parallel side-channel at every layer. An exhausted close with the position still on-chain now terminates as an **involuntary `POSITION_HOLD`** with `hold_reason: "close_retries_exhausted"` and a zero-amount marker order carrying the position address — riding the existing hold machinery (DB-recovered on the API path) instead of a bespoke flag. `FAILED` is reserved for exhaustion with nothing left on-chain (e.g. an open rejected at simulation). ### Controller, hummingbot-api, condor @@ -106,15 +106,13 @@ sequenceDiagram EX->>EX: already closed → COMPLETE else position live or read transiently failed EX->>CN: close position - loop ≤ 3 inner attempts on SLIPPAGE_EXCEEDED / SIMULATION_FAILED / TX_NOT_CONFIRMED - CN->>GW: POST close-position - GW->>SOL: fetch fresh state → quote → build → simulate - alt simulation fails - GW-->>CN: typed 400, pre-broadcast — no fee spent - else - GW->>SOL: broadcast + confirm - GW-->>CN: CONFIRMED or typed error - end + CN->>GW: POST close-position (one request — max_retries=0) + GW->>SOL: fetch fresh state → quote → build → simulate + alt simulation fails + GW-->>CN: typed 400, pre-broadcast — no fee spent + else + GW->>SOL: broadcast + confirm + GW-->>CN: CONFIRMED or typed error end CN-->>EX: result or typed exception end @@ -180,14 +178,14 @@ Also listed as orphan candidates: legacy `FAILED` executors whose final state ca ### 3.5 Transaction status polling -Gateway's `/poll` reports one of four statuses; the connector's 1 s poller acts on them: +Both chains' `/poll` routes share one `TransactionStatusCode` contract; the connector's 1 s poller acts on it: | `txStatus` | Meaning | Connector behavior | |---|---|---| | `1` CONFIRMED | Landed without error | Fill the order | -| `0` PENDING | The chain has seen the transaction | Keep polling — it can still confirm (unbounded by design) | -| `-1` FAILED | Landed with an error | Fail the order, trigger `TransactionFailure` | -| `-2` NOT_FOUND | Unknown to the chain — never received or dropped | Transient while the order is younger than 120 s (blockhash validity margin); afterwards each miss counts toward the tracker's lost-order limit → order fails after repeated consecutive misses | +| `0` PENDING | The chain has seen the transaction (Solana: signature status; EVM: in the mempool) | Keep polling — it can still confirm (unbounded by design) | +| `-1` FAILED | Landed with an error / reverted | Fail the order, trigger `TransactionFailure` | +| `-2` NOT_FOUND | Unknown to the chain — never received or dropped (Solana: terminal once the blockhash expires; EVM: the mempool is visible via `getTransaction`, so not-found already means dropped) | Transient while the order is younger than 120 s (blockhash validity margin); afterwards each miss counts toward the tracker's lost-order limit → order fails after repeated consecutive misses | Transient poll errors (RPC failures) report `PENDING`, never `NOT_FOUND` — an unknown outcome is a reason to poll again, not to give up. Note the LP flow's writes do not depend on this poller to progress: the connector's operation calls only return a signature after in-request confirmation. The bound matters for flows that record a hash at submission time, and for the RPC cost of stuck orders. From 546c643816a33d8c1b5f66c1710fd10101b45de8 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 08:58:24 -0700 Subject: [PATCH 06/31] fix(poll): attribute failed-transaction errors to the failing program MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/chains/solana/routes/poll.ts | 7 +++++-- test/chains/solana/routes/poll.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/chains/solana/routes/poll.ts b/src/chains/solana/routes/poll.ts index dc55498f29..3351d3b6cc 100644 --- a/src/chains/solana/routes/poll.ts +++ b/src/chains/solana/routes/poll.ts @@ -60,10 +60,13 @@ export async function pollSolanaTransaction( // Extract fee from transaction const fee = txData.meta?.fee ? txData.meta.fee / 1e9 : 0; // Convert lamports to SOL - // Check for transaction error and parse it + // Check for transaction error and parse it. The err object carries the code but + // names no program, so parse it together with the program logs — attribution + // comes from the "Program X failed: custom program error" line, and without it + // every program-specific code (e.g. Orca 6018) falls through to UNKNOWN. let error: string | null = null; if (txData.meta?.err) { - const errorStr = JSON.stringify(txData.meta.err); + const errorStr = [JSON.stringify(txData.meta.err), ...(txData.meta.logMessages ?? [])].join('\n'); const parsed = parseSolanaError(errorStr); error = `${parsed.type} (${parsed.errorCodeHex || 'unknown'}): ${parsed.message}`; logger.info(`Transaction ${signature} failed: ${error}`); diff --git a/test/chains/solana/routes/poll.test.ts b/test/chains/solana/routes/poll.test.ts index 6e123e6eb0..f5bd3b422e 100644 --- a/test/chains/solana/routes/poll.test.ts +++ b/test/chains/solana/routes/poll.test.ts @@ -47,6 +47,31 @@ describe('pollSolanaTransaction', () => { expect(result.error).toBeNull(); }); + it('attributes a failed transaction to the program named in the logs', async () => { + // meta.err carries the code but no program; without the logs the parser cannot + // reach the program's error table and every custom code reports UNKNOWN. + mockSolanaInstance.getTransaction.mockResolvedValue({ + slot: 365794000, + meta: { + fee: 5000, + err: { InstructionError: [0, { Custom: 6018 }] }, + logMessages: [ + 'Program ComputeBudget111111111111111111111111111111 invoke [1]', + 'Program ComputeBudget111111111111111111111111111111 success', + 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc invoke [1]', + 'Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc failed: custom program error: 0x1782', + ], + }, + }); + mockSolanaInstance.getTransactionStatusCode.mockResolvedValue(TransactionStatusCode.FAILED); + + const result = await pollSolanaTransaction(null as any, 'mainnet-beta', VALID_SIGNATURE); + + expect(result.txStatus).toBe(TransactionStatusCode.FAILED); + expect(result.error).toContain('SLIPPAGE_EXCEEDED'); + expect(result.error).not.toContain('UNKNOWN'); + }); + describe('null txData (not visible at confirmed commitment)', () => { beforeEach(() => { mockSolanaInstance.getTransaction.mockResolvedValue(null); From 2452a89c6c054a1ebc798f14817329dcea0af660 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 09:49:20 -0700 Subject: [PATCH 07/31] feat(clmm): binCount on unified pool-info; add bin support to PancakeSwap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/connectors/clmm-v3-utils.ts | 138 ++++++++++++++ .../pancakeswap/clmm-routes/poolInfo.ts | 63 +++++-- src/connectors/pancakeswap/schemas.ts | 13 +- src/connectors/uniswap/uniswap.utils.ts | 142 ++++---------- src/trading/clmm/pools.ts | 31 +++- .../pancakeswap/clmm-routes/pool-info.test.ts | 174 ++++++++++++++++++ test/trading/clmm/pool-info-bin-count.test.ts | 124 +++++++++++++ 7 files changed, 560 insertions(+), 125 deletions(-) create mode 100644 src/connectors/clmm-v3-utils.ts create mode 100644 test/connectors/pancakeswap/clmm-routes/pool-info.test.ts create mode 100644 test/trading/clmm/pool-info-bin-count.test.ts diff --git a/src/connectors/clmm-v3-utils.ts b/src/connectors/clmm-v3-utils.ts new file mode 100644 index 0000000000..de831a1340 --- /dev/null +++ b/src/connectors/clmm-v3-utils.ts @@ -0,0 +1,138 @@ +import { Contract } from '@ethersproject/contracts'; + +import { BinLiquidity } from '../schemas/clmm-schema'; + +/** + * Per-bin liquidity distribution around the current tick for a Uniswap-V3-style + * pool. Shared by the Uniswap and PancakeSwap connectors — PancakeSwap V3 is a + * Uniswap V3 fork with identical tick semantics, so the walk is the same; each + * connector passes its own SDK's math implementations rather than depending on + * the other's. + * + * V3 has no equivalent of Orca's "fetch all positions for pool" RPC — positions + * are NFTs on the position manager. Instead we walk the pool's per-tick + * liquidity profile directly: + * + * 1. Read `liquidityNet` at every bin boundary in the window via parallel + * `pool.ticks(tick)` reads (one eth_call each, fired with Promise.all). + * Ticks that have never been initialized return zeros — that's harmless. + * 2. Start with the pool's active L = pool.liquidity() in the bin that + * contains the current tick. Propagate L outward by adding/subtracting + * `liquidityNet` at each boundary crossed (per the V3 spec). + * 3. For each bin, convert L → (amount0, amount1) via + * getAmount{0,1}Delta(sqrtA, sqrtB, L, false), splitting at the pool's + * current sqrtPriceX96 when the bin straddles the active tick. + * 4. Map to base/quote using `isBaseToken0`, scale by decimals. + * + * Output shape mirrors Meteora's `pool-info.bins[]`. + */ + +// Structural types for the V3 SDK math, in native bigint. The SDKs disagree on +// their numeric type — @uniswap/v3-sdk is JSBI-based, @pancakeswap/v3-sdk uses +// native bigint — so each connector adapts its own SDK to this interface rather +// than one connector importing the other's math. +export interface V3TickMath { + getSqrtRatioAtTick(tick: number): bigint; +} + +export interface V3SqrtPriceMath { + getAmount0Delta(sqrtRatioAX96: bigint, sqrtRatioBX96: bigint, liquidity: bigint, roundUp: boolean): bigint; + getAmount1Delta(sqrtRatioAX96: bigint, sqrtRatioBX96: bigint, liquidity: bigint, roundUp: boolean): bigint; +} + +export async function computeV3BinDistribution(args: { + poolContract: Contract; + tickSpacing: number; + currentTick: number; + currentSqrtPriceX96: bigint; + activeLiquidity: bigint; + decimals0: number; + decimals1: number; + isBaseToken0: boolean; + binCount: number; + tickMath: V3TickMath; + sqrtPriceMath: V3SqrtPriceMath; +}): Promise { + const { + poolContract, + tickSpacing, + currentTick, + currentSqrtPriceX96, + activeLiquidity, + decimals0, + decimals1, + isBaseToken0, + binCount, + tickMath, + sqrtPriceMath, + } = args; + if (binCount <= 0) return []; + + const halfBins = Math.floor(binCount / 2); + const snapped = Math.floor(currentTick / tickSpacing) * tickSpacing; + const firstBinStart = snapped - halfBins * tickSpacing; + const boundaries: number[] = []; + for (let i = 0; i <= binCount; i++) { + boundaries.push(firstBinStart + i * tickSpacing); + } + + // Parallel reads of pool.ticks(tick) at each boundary. Non-initialized + // ticks return zeros which is the correct neutral element for liquidityNet. + const tickData = await Promise.all( + boundaries.map((tick) => poolContract.ticks(tick).catch(() => ({ liquidityNet: 0 }))), + ); + + const curIdx = Math.floor((currentTick - firstBinStart) / tickSpacing); + + // Propagate L outward from the current bin (V3 spec: crossing a tick going + // UP adds liquidityNet, going DOWN subtracts it). + const binLs: bigint[] = new Array(binCount); + binLs[curIdx] = activeLiquidity; + for (let i = curIdx + 1; i < binCount; i++) { + const net = BigInt(tickData[i].liquidityNet.toString()); + binLs[i] = binLs[i - 1] + net; + } + for (let i = curIdx - 1; i >= 0; i--) { + const net = BigInt(tickData[i + 1].liquidityNet.toString()); + binLs[i] = binLs[i + 1] - net; + } + + const scale0 = Math.pow(10, decimals0); + const scale1 = Math.pow(10, decimals1); + const zero = 0n; + const bins: BinLiquidity[] = []; + for (let i = 0; i < binCount; i++) { + const tickStart = boundaries[i]; + const tickEnd = boundaries[i + 1]; + const L = binLs[i]; + let amount0: bigint = zero; + let amount1: bigint = zero; + if (L > zero) { + const sqrtA = tickMath.getSqrtRatioAtTick(tickStart); + const sqrtB = tickMath.getSqrtRatioAtTick(tickEnd); + if (currentTick >= tickEnd) { + amount1 = sqrtPriceMath.getAmount1Delta(sqrtA, sqrtB, L, false); + } else if (currentTick < tickStart) { + amount0 = sqrtPriceMath.getAmount0Delta(sqrtA, sqrtB, L, false); + } else { + amount0 = sqrtPriceMath.getAmount0Delta(currentSqrtPriceX96, sqrtB, L, false); + amount1 = sqrtPriceMath.getAmount1Delta(sqrtA, currentSqrtPriceX96, L, false); + } + } + const amt0 = parseFloat(amount0.toString()) / scale0; + const amt1 = parseFloat(amount1.toString()) / scale1; + const baseTokenAmount = isBaseToken0 ? amt0 : amt1; + const quoteTokenAmount = isBaseToken0 ? amt1 : amt0; + + // Price at tickStart in human units (quote/base regardless of token order). + const rawT1PerT0 = Math.pow(1.0001, tickStart) * Math.pow(10, decimals0 - decimals1); + const price = isBaseToken0 ? rawT1PerT0 : 1 / rawT1PerT0; + bins.push({ + binId: tickStart, + price, + baseTokenAmount, + quoteTokenAmount, + }); + } + return bins; +} diff --git a/src/connectors/pancakeswap/clmm-routes/poolInfo.ts b/src/connectors/pancakeswap/clmm-routes/poolInfo.ts index 8efd314f11..ae0c5713c3 100644 --- a/src/connectors/pancakeswap/clmm-routes/poolInfo.ts +++ b/src/connectors/pancakeswap/clmm-routes/poolInfo.ts @@ -1,13 +1,23 @@ +import { Contract as EthersProjectContract } from '@ethersproject/contracts'; +import { abi as IPancakeV3PoolABI } from '@pancakeswap/v3-core/artifacts/contracts/interfaces/IPancakeV3Pool.sol/IPancakeV3Pool.json'; +import { SqrtPriceMath, TickMath } from '@pancakeswap/v3-sdk'; import { FastifyPluginAsync, FastifyInstance } from 'fastify'; -import { GetPoolInfoRequestType, PoolInfo, PoolInfoSchema } from '../../../schemas/clmm-schema'; +import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { PoolInfo, PoolInfoSchema } from '../../../schemas/clmm-schema'; import { logger } from '../../../services/logger'; import { sanitizeErrorMessage } from '../../../services/sanitize'; +import { computeV3BinDistribution } from '../../clmm-v3-utils'; import { Pancakeswap } from '../pancakeswap'; import { formatTokenAmount, getPancakeswapPoolInfo } from '../pancakeswap.utils'; -import { PancakeswapClmmGetPoolInfoRequest } from '../schemas'; - -export async function getPoolInfo(fastify: FastifyInstance, network: string, poolAddress: string): Promise { +import { PancakeswapClmmGetPoolInfoRequest, PancakeswapClmmGetPoolInfoRequestType } from '../schemas'; + +export async function getPoolInfo( + fastify: FastifyInstance, + network: string, + poolAddress: string, + binCount: number = 0, +): Promise { const pancakeswap = await Pancakeswap.getInstance(network); if (!poolAddress) { @@ -41,9 +51,18 @@ export async function getPoolInfo(fastify: FastifyInstance, network: string, poo const price = isBaseToken0 ? parseFloat(price0) : parseFloat(price1); - const liquidity = pool.liquidity; - const token0Amount = formatTokenAmount(liquidity.toString(), token0.decimals); - const token1Amount = formatTokenAmount(liquidity.toString(), token1.decimals); + // Read the pool contract's actual ERC20 balances. V3's `pool.liquidity` is the + // active virtual liquidity in sqrt-price space, not a token amount — using it + // reported the same figure for both sides, scaled by each token's decimals. + const ethereum = await Ethereum.getInstance(network); + const token0Contract = ethereum.getContract(token0.address, ethereum.provider); + const token1Contract = ethereum.getContract(token1.address, ethereum.provider); + const [token0Balance, token1Balance] = await Promise.all([ + ethereum.getERC20BalanceByAddress(token0Contract, poolAddress, token0.decimals), + ethereum.getERC20BalanceByAddress(token1Contract, poolAddress, token1.decimals), + ]); + const token0Amount = formatTokenAmount(token0Balance.value.toString(), token0.decimals); + const token1Amount = formatTokenAmount(token1Balance.value.toString(), token1.decimals); const baseTokenAmount = isBaseToken0 ? token0Amount : token1Amount; const quoteTokenAmount = isBaseToken0 ? token1Amount : token0Amount; @@ -52,7 +71,7 @@ export async function getPoolInfo(fastify: FastifyInstance, network: string, poo const tickSpacing = pool.tickSpacing; const activeBinId = pool.tickCurrent; - return { + const result: PoolInfo = { address: poolAddress, baseTokenAddress: baseTokenObj.address, quoteTokenAddress: quoteTokenObj.address, @@ -63,11 +82,33 @@ export async function getPoolInfo(fastify: FastifyInstance, network: string, poo quoteTokenAmount: quoteTokenAmount, activeBinId: activeBinId, }; + + // Optionally include the per-bin distribution around the current tick. + // Fires N parallel pool.ticks(tick) reads — only when binCount > 0 so the + // default pool-info latency is unaffected. + if (binCount > 0) { + const poolContract = new EthersProjectContract(poolAddress, IPancakeV3PoolABI, ethereum.provider); + result.bins = await computeV3BinDistribution({ + poolContract, + tickSpacing, + currentTick: activeBinId, + currentSqrtPriceX96: BigInt(pool.sqrtRatioX96.toString()), + activeLiquidity: BigInt(pool.liquidity.toString()), + decimals0: token0.decimals, + decimals1: token1.decimals, + isBaseToken0, + binCount, + tickMath: TickMath, + sqrtPriceMath: SqrtPriceMath, + }); + } + + return result; } export const poolInfoRoute: FastifyPluginAsync = async (fastify) => { fastify.get<{ - Querystring: GetPoolInfoRequestType; + Querystring: PancakeswapClmmGetPoolInfoRequestType; Reply: Record; }>( '/pool-info', @@ -83,9 +124,9 @@ export const poolInfoRoute: FastifyPluginAsync = async (fastify) => { }, async (request): Promise => { try { - const { poolAddress } = request.query; + const { poolAddress, binCount = 0 } = request.query; const network = request.query.network; - return await getPoolInfo(fastify, network, poolAddress); + return await getPoolInfo(fastify, network, poolAddress, binCount); } catch (e) { logger.error(e); if (e.statusCode) { diff --git a/src/connectors/pancakeswap/schemas.ts b/src/connectors/pancakeswap/schemas.ts index 7d01b9a1cf..ba5b9ec143 100644 --- a/src/connectors/pancakeswap/schemas.ts +++ b/src/connectors/pancakeswap/schemas.ts @@ -1,4 +1,4 @@ -import { Type } from '@sinclair/typebox'; +import { Type, Static } from '@sinclair/typebox'; import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; @@ -110,7 +110,18 @@ export const PancakeswapClmmGetPoolInfoRequest = Type.Object({ description: 'Pancakeswap V3 pool address', examples: [CLMM_POOL_ADDRESS_EXAMPLE], }), + binCount: Type.Optional( + Type.Integer({ + description: + 'If > 0, include a `bins` array (per-tickSpacing token amounts around the current tick), ' + + 'mirroring Meteora pool-info.bins[]. Default 0 — pool-info skips the extra eth_calls.', + default: 0, + minimum: 0, + maximum: 401, + }), + ), }); +export type PancakeswapClmmGetPoolInfoRequestType = Static; // Pancakeswap CLMM Create Pool Request (Pancakeswap V3 — Uniswap V3 fork) export const PancakeswapClmmCreatePoolRequest = Type.Object({ diff --git a/src/connectors/uniswap/uniswap.utils.ts b/src/connectors/uniswap/uniswap.utils.ts index eb24b37819..787f8cadf7 100644 --- a/src/connectors/uniswap/uniswap.utils.ts +++ b/src/connectors/uniswap/uniswap.utils.ts @@ -7,7 +7,9 @@ import { FastifyInstance } from 'fastify'; import JSBI from 'jsbi'; import { TokenInfo, Ethereum } from '../../chains/ethereum/ethereum'; +import { BinLiquidity } from '../../schemas/clmm-schema'; import { logger } from '../../services/logger'; +import { computeV3BinDistribution, V3SqrtPriceMath, V3TickMath } from '../clmm-v3-utils'; import { Uniswap } from './uniswap'; import { UniswapConfig } from './uniswap.config'; @@ -304,32 +306,35 @@ export async function getUniswapPoolInfo( /** * Per-bin liquidity distribution around the current tick for a Uniswap V3 pool. - * - * Uniswap V3 has no equivalent of Orca's "fetch all positions for pool" RPC — - * positions are NFTs on the NonfungiblePositionManager. Instead we walk the - * pool's per-tick liquidity profile directly: - * - * 1. Read `liquidityNet` at every bin boundary in the window via parallel - * `pool.ticks(tick)` reads (one eth_call each, fired with Promise.all). - * Ticks that have never been initialized return zeros — that's harmless. - * 2. Start with the pool's active L = pool.liquidity() in the bin that - * contains the current tick. Propagate L outward by adding/subtracting - * `liquidityNet` at each boundary crossed (per the V3 spec). - * 3. For each bin, convert L → (amount0, amount1) via - * SqrtPriceMath.getAmount{0,1}Delta(sqrtA, sqrtB, L, false), splitting - * at the pool's current sqrtPriceX96 when the bin straddles the - * active tick. - * 4. Map to base/quote using `isBaseToken0`, scale by decimals. - * - * Output shape mirrors Meteora's `pool-info.bins[]`: - * { binId, price, baseTokenAmount, quoteTokenAmount } + * The tick walk lives in the shared V3 helper (`clmm-v3-utils`); Uniswap's SDK is + * JSBI-based while the helper works in native bigint, so the math is adapted here. */ -export interface UniswapBinDistributionEntry { - binId: number; - price: number; - baseTokenAmount: number; - quoteTokenAmount: number; -} +export type UniswapBinDistributionEntry = BinLiquidity; + +const uniswapTickMath: V3TickMath = { + getSqrtRatioAtTick: (tick) => BigInt(TickMath.getSqrtRatioAtTick(tick).toString()), +}; + +const uniswapSqrtPriceMath: V3SqrtPriceMath = { + getAmount0Delta: (a, b, l, roundUp) => + BigInt( + SqrtPriceMath.getAmount0Delta( + JSBI.BigInt(a.toString()), + JSBI.BigInt(b.toString()), + JSBI.BigInt(l.toString()), + roundUp, + ).toString(), + ), + getAmount1Delta: (a, b, l, roundUp) => + BigInt( + SqrtPriceMath.getAmount1Delta( + JSBI.BigInt(a.toString()), + JSBI.BigInt(b.toString()), + JSBI.BigInt(l.toString()), + roundUp, + ).toString(), + ), +}; export async function computeUniswapBinDistribution(args: { poolContract: Contract; @@ -342,84 +347,11 @@ export async function computeUniswapBinDistribution(args: { isBaseToken0: boolean; binCount: number; }): Promise { - const { - poolContract, - tickSpacing, - currentTick, - currentSqrtPriceX96, - activeLiquidity, - decimals0, - decimals1, - isBaseToken0, - binCount, - } = args; - if (binCount <= 0) return []; - - const halfBins = Math.floor(binCount / 2); - const snapped = Math.floor(currentTick / tickSpacing) * tickSpacing; - const firstBinStart = snapped - halfBins * tickSpacing; - const boundaries: number[] = []; - for (let i = 0; i <= binCount; i++) { - boundaries.push(firstBinStart + i * tickSpacing); - } - - // Parallel reads of pool.ticks(tick) at each boundary. Non-initialized - // ticks return zeros which is the correct neutral element for liquidityNet. - const tickData = await Promise.all( - boundaries.map((tick) => poolContract.ticks(tick).catch(() => ({ liquidityNet: 0 }))), - ); - - const curIdx = Math.floor((currentTick - firstBinStart) / tickSpacing); - - // Propagate L outward from the current bin (V3 spec: crossing a tick going - // UP adds liquidityNet, going DOWN subtracts it). - const binLs: JSBI[] = new Array(binCount); - binLs[curIdx] = activeLiquidity; - for (let i = curIdx + 1; i < binCount; i++) { - const net = JSBI.BigInt(tickData[i].liquidityNet.toString()); - binLs[i] = JSBI.add(binLs[i - 1], net); - } - for (let i = curIdx - 1; i >= 0; i--) { - const net = JSBI.BigInt(tickData[i + 1].liquidityNet.toString()); - binLs[i] = JSBI.subtract(binLs[i + 1], net); - } - - const scale0 = Math.pow(10, decimals0); - const scale1 = Math.pow(10, decimals1); - const zero = JSBI.BigInt(0); - const bins: UniswapBinDistributionEntry[] = []; - for (let i = 0; i < binCount; i++) { - const tickStart = boundaries[i]; - const tickEnd = boundaries[i + 1]; - const L = binLs[i]; - let amount0: JSBI = zero; - let amount1: JSBI = zero; - if (JSBI.greaterThan(L, zero)) { - const sqrtA = TickMath.getSqrtRatioAtTick(tickStart); - const sqrtB = TickMath.getSqrtRatioAtTick(tickEnd); - if (currentTick >= tickEnd) { - amount1 = SqrtPriceMath.getAmount1Delta(sqrtA, sqrtB, L, false); - } else if (currentTick < tickStart) { - amount0 = SqrtPriceMath.getAmount0Delta(sqrtA, sqrtB, L, false); - } else { - amount0 = SqrtPriceMath.getAmount0Delta(currentSqrtPriceX96, sqrtB, L, false); - amount1 = SqrtPriceMath.getAmount1Delta(sqrtA, currentSqrtPriceX96, L, false); - } - } - const amt0 = parseFloat(amount0.toString()) / scale0; - const amt1 = parseFloat(amount1.toString()) / scale1; - const baseTokenAmount = isBaseToken0 ? amt0 : amt1; - const quoteTokenAmount = isBaseToken0 ? amt1 : amt0; - - // Price at tickStart in human units (quote/base regardless of token order). - const rawT1PerT0 = Math.pow(1.0001, tickStart) * Math.pow(10, decimals0 - decimals1); - const price = isBaseToken0 ? rawT1PerT0 : 1 / rawT1PerT0; - bins.push({ - binId: tickStart, - price, - baseTokenAmount, - quoteTokenAmount, - }); - } - return bins; + return computeV3BinDistribution({ + ...args, + currentSqrtPriceX96: BigInt(args.currentSqrtPriceX96.toString()), + activeLiquidity: BigInt(args.activeLiquidity.toString()), + tickMath: uniswapTickMath, + sqrtPriceMath: uniswapSqrtPriceMath, + }); } diff --git a/src/trading/clmm/pools.ts b/src/trading/clmm/pools.ts index bd35c68926..c6c8c8d199 100644 --- a/src/trading/clmm/pools.ts +++ b/src/trading/clmm/pools.ts @@ -34,6 +34,17 @@ const UnifiedPoolInfoRequestSchema = Type.Object({ description: 'Pool contract address', examples: [CLMM_POOL_ADDRESS_EXAMPLE], }), + binCount: Type.Optional( + Type.Integer({ + description: + 'If > 0, include a `bins` array of per-tick liquidity around the active tick. Supported by ' + + 'orca, raydium, uniswap and pancakeswap; Meteora always returns its bins and ignores this. ' + + 'Default 0 = skip the bin fetch.', + default: 0, + minimum: 0, + maximum: 401, + }), + ), }); type UnifiedPoolInfoRequest = Static; @@ -64,18 +75,20 @@ async function getSolanaPoolInfo( connector: string, network: string, poolAddress: string, + binCount: number, ): Promise { logger.info(`[CLMM] Getting pool info from ${connector} on solana/${network}`); switch (connector) { case 'raydium': - return await raydiumGetPoolInfo(fastify, network, poolAddress); + return await raydiumGetPoolInfo(fastify, network, poolAddress, binCount); case 'meteora': + // Meteora always returns its bins; it has no binCount parameter. return await meteoraGetPoolInfo(fastify, network, poolAddress); case 'pancakeswap-sol': return await pancakeswapSolGetPoolInfo(fastify, network, poolAddress); case 'orca': - return await orcaGetPoolInfo(fastify, network, poolAddress); + return await orcaGetPoolInfo(fastify, network, poolAddress, binCount); default: throw fastify.httpErrors.badRequest(`Unsupported Solana CLMM connector: ${connector}`); } @@ -89,14 +102,15 @@ async function getEthereumPoolInfo( connector: string, network: string, poolAddress: string, + binCount: number, ): Promise { logger.info(`[CLMM] Getting pool info from ${connector} on ethereum/${network}`); switch (connector) { case 'uniswap': - return await uniswapGetPoolInfo(fastify, network, poolAddress); + return await uniswapGetPoolInfo(fastify, network, poolAddress, binCount); case 'pancakeswap': - return await pancakeswapGetPoolInfo(fastify, network, poolAddress); + return await pancakeswapGetPoolInfo(fastify, network, poolAddress, binCount); default: throw fastify.httpErrors.badRequest(`Unsupported Ethereum CLMM connector: ${connector}`); } @@ -110,6 +124,7 @@ export async function getUnifiedPoolInfo( connector: string, chainNetwork: string, poolAddress: string, + binCount: number = 0, ): Promise { const { chain, network } = parseChainNetwork(chainNetwork); @@ -117,10 +132,10 @@ export async function getUnifiedPoolInfo( switch (chain.toLowerCase()) { case 'ethereum': - return getEthereumPoolInfo(fastify, connector, network, poolAddress); + return getEthereumPoolInfo(fastify, connector, network, poolAddress, binCount); case 'solana': - return getSolanaPoolInfo(fastify, connector, network, poolAddress); + return getSolanaPoolInfo(fastify, connector, network, poolAddress, binCount); default: throw fastify.httpErrors.badRequest(`Unsupported chain: ${chain}`); @@ -148,10 +163,10 @@ export const poolsRoute: FastifyPluginAsync = async (fastify) => { }, }, async (request, reply) => { - const { connector, chainNetwork, poolAddress } = request.query; + const { connector, chainNetwork, poolAddress, binCount = 0 } = request.query; try { - const result = await getUnifiedPoolInfo(fastify, connector, chainNetwork, poolAddress); + const result = await getUnifiedPoolInfo(fastify, connector, chainNetwork, poolAddress, binCount); return reply.code(200).send(result); } catch (error: any) { logger.error(`[UnifiedCLMM] Pool info error: ${error.message}`); diff --git a/test/connectors/pancakeswap/clmm-routes/pool-info.test.ts b/test/connectors/pancakeswap/clmm-routes/pool-info.test.ts new file mode 100644 index 0000000000..3f011df491 --- /dev/null +++ b/test/connectors/pancakeswap/clmm-routes/pool-info.test.ts @@ -0,0 +1,174 @@ +import { BigNumber } from 'ethers'; + +import { Ethereum } from '../../../../src/chains/ethereum/ethereum'; +import { fastifyWithTypeProvider } from '../../../utils/testUtils'; + +jest.mock('../../../../src/chains/ethereum/ethereum'); +jest.mock('../../../../src/connectors/pancakeswap/pancakeswap'); +jest.mock('../../../../src/connectors/pancakeswap/pancakeswap.utils'); +jest.mock('../../../../src/connectors/clmm-v3-utils'); + +const buildApp = async () => { + const server = fastifyWithTypeProvider(); + await server.register(require('@fastify/sensible')); + const { poolInfoRoute } = await import('../../../../src/connectors/pancakeswap/clmm-routes/poolInfo'); + await server.register(poolInfoRoute); + return server; +}; + +// PancakeSwap V3 USDT-WBNB pool on BSC. +const POOL_ADDRESS = '0x172fcd41e0913e95784454622d1c3724f546f849'; +const USDT = { address: '0x55d398326f99059fF775485246999027B3197955', symbol: 'USDT', decimals: 18 }; +const WBNB = { address: '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c', symbol: 'WBNB', decimals: 18 }; + +const USDT_RAW_BALANCE = BigNumber.from('4200000000000000000000000'); // 4,200,000 * 1e18 +const WBNB_RAW_BALANCE = BigNumber.from('7000000000000000000000'); // 7,000 * 1e18 + +// V3 virtual liquidity — the value the route used to report for BOTH tokens. +const POOL_LIQUIDITY = BigNumber.from('11034936417288527'); + +const mockPool = { + token0: { address: USDT.address, decimals: USDT.decimals }, + token1: { address: WBNB.address, decimals: WBNB.decimals }, + liquidity: POOL_LIQUIDITY, + sqrtRatioX96: BigNumber.from('79228162514264337593543950336'), + token0Price: { toSignificant: () => '0.00166' }, // USDT priced in WBNB + token1Price: { toSignificant: () => '602.4' }, + fee: 2500, // 0.25% in hundredths-of-bips + tickSpacing: 50, + tickCurrent: -64000, +}; + +const setupMocks = async () => { + const { Pancakeswap } = await import('../../../../src/connectors/pancakeswap/pancakeswap'); + const { getPancakeswapPoolInfo, formatTokenAmount } = await import( + '../../../../src/connectors/pancakeswap/pancakeswap.utils' + ); + + (getPancakeswapPoolInfo as jest.Mock).mockResolvedValue({ + baseTokenAddress: USDT.address, + quoteTokenAddress: WBNB.address, + poolType: 'clmm', + }); + (formatTokenAmount as jest.Mock).mockImplementation( + (amount: string, decimals: number) => Number(amount) / Math.pow(10, decimals), + ); + + (Pancakeswap.getInstance as jest.Mock).mockResolvedValue({ + getToken: jest.fn().mockImplementation((addr: string) => { + if (addr.toLowerCase() === USDT.address.toLowerCase()) return USDT; + if (addr.toLowerCase() === WBNB.address.toLowerCase()) return WBNB; + return null; + }), + getV3Pool: jest.fn().mockResolvedValue(mockPool), + }); + + const mockUsdtContract = { address: USDT.address }; + const mockWbnbContract = { address: WBNB.address }; + (Ethereum.getInstance as jest.Mock).mockResolvedValue({ + provider: { _isProvider: true }, + getContract: jest.fn().mockImplementation((tokenAddress: string) => { + if (tokenAddress.toLowerCase() === USDT.address.toLowerCase()) return mockUsdtContract; + if (tokenAddress.toLowerCase() === WBNB.address.toLowerCase()) return mockWbnbContract; + throw new Error(`unexpected contract address ${tokenAddress}`); + }), + getERC20BalanceByAddress: jest.fn().mockImplementation((contract: any, address: string, decimals: number) => { + expect(address).toBe(POOL_ADDRESS); // route must query the pool contract + if (contract.address === USDT.address) return Promise.resolve({ value: USDT_RAW_BALANCE, decimals }); + if (contract.address === WBNB.address) return Promise.resolve({ value: WBNB_RAW_BALANCE, decimals }); + return Promise.reject(new Error('unexpected token')); + }), + }); +}; + +const sampleBins = (n: number) => + Array.from({ length: n }, (_, i) => ({ + binId: -64000 + i * 50, + price: 600 + i, + baseTokenAmount: 10 + i, + quoteTokenAmount: 20 + i, + })); + +describe('GET /pool-info (PancakeSwap CLMM)', () => { + let server: any; + + beforeAll(async () => { + server = await buildApp(); + }); + + afterAll(async () => { + await server.close(); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + await setupMocks(); + }); + + it("returns the pool contract's actual ERC20 balances, not pool.liquidity", async () => { + const response = await server.inject({ + method: 'GET', + url: '/pool-info', + query: { network: 'bsc', poolAddress: POOL_ADDRESS }, + }); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + + expect(body.address).toBe(POOL_ADDRESS); + expect(body.feePct).toBeCloseTo(0.25, 6); + expect(body.binStep).toBe(50); + expect(body.activeBinId).toBe(-64000); + + // The fix: token amounts come from ERC20 balanceOf, not virtual liquidity. + expect(body.baseTokenAmount).toBeCloseTo(4200000, 0); + expect(body.quoteTokenAmount).toBeCloseTo(7000, 0); + + // Regression guard: the legacy bug reported pool.liquidity / 10^decimals for + // both sides — identical figures that mean nothing in token terms. + const buggy = Number(POOL_LIQUIDITY.toString()) / 1e18; + expect(body.baseTokenAmount).not.toBeCloseTo(buggy, 6); + expect(body.quoteTokenAmount).not.toBeCloseTo(buggy, 6); + expect(body.baseTokenAmount).not.toEqual(body.quoteTokenAmount); + }); + + it('omits bins and skips the tick reads when binCount is absent', async () => { + const { computeV3BinDistribution } = await import('../../../../src/connectors/clmm-v3-utils'); + + const response = await server.inject({ + method: 'GET', + url: '/pool-info', + query: { network: 'bsc', poolAddress: POOL_ADDRESS }, + }); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.body).bins).toBeUndefined(); + expect(computeV3BinDistribution).not.toHaveBeenCalled(); + }); + + it('returns bins when binCount > 0', async () => { + const { computeV3BinDistribution } = await import('../../../../src/connectors/clmm-v3-utils'); + (computeV3BinDistribution as jest.Mock).mockResolvedValueOnce(sampleBins(11)); + + const response = await server.inject({ + method: 'GET', + url: '/pool-info', + query: { network: 'bsc', poolAddress: POOL_ADDRESS, binCount: '11' }, + }); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + expect(body.bins).toHaveLength(11); + expect(body.bins[0]).toEqual({ binId: -64000, price: 600, baseTokenAmount: 10, quoteTokenAmount: 20 }); + + expect(computeV3BinDistribution).toHaveBeenCalledTimes(1); + const args = (computeV3BinDistribution as jest.Mock).mock.calls[0][0]; + expect(args.binCount).toBe(11); + expect(args.tickSpacing).toBe(50); + expect(args.currentTick).toBe(-64000); + expect(args.isBaseToken0).toBe(true); + // PancakeSwap's SDK math must be supplied — the shared helper is math-agnostic. + expect(typeof args.tickMath.getSqrtRatioAtTick).toBe('function'); + expect(typeof args.sqrtPriceMath.getAmount0Delta).toBe('function'); + }); +}); diff --git a/test/trading/clmm/pool-info-bin-count.test.ts b/test/trading/clmm/pool-info-bin-count.test.ts new file mode 100644 index 0000000000..cdef04a21a --- /dev/null +++ b/test/trading/clmm/pool-info-bin-count.test.ts @@ -0,0 +1,124 @@ +// binCount must reach the connector from the unified route. It was silently +// dropped: the querystring schema had no binCount and the connector calls passed +// only (fastify, network, poolAddress), so `bins` could never be returned through +// /trading/clmm/pool-info even for connectors that support it. + +const orcaGetPoolInfo = jest.fn(); +const raydiumGetPoolInfo = jest.fn(); +const uniswapGetPoolInfo = jest.fn(); +const pancakeswapGetPoolInfo = jest.fn(); +const meteoraGetPoolInfo = jest.fn(); +const pancakeswapSolGetPoolInfo = jest.fn(); + +jest.mock('../../../src/connectors/orca/clmm-routes/poolInfo', () => ({ getPoolInfo: orcaGetPoolInfo })); +jest.mock('../../../src/connectors/raydium/clmm-routes/poolInfo', () => ({ getPoolInfo: raydiumGetPoolInfo })); +jest.mock('../../../src/connectors/uniswap/clmm-routes/poolInfo', () => ({ getPoolInfo: uniswapGetPoolInfo })); +jest.mock('../../../src/connectors/pancakeswap/clmm-routes/poolInfo', () => ({ getPoolInfo: pancakeswapGetPoolInfo })); +jest.mock('../../../src/connectors/meteora/clmm-routes/poolInfo', () => ({ getPoolInfo: meteoraGetPoolInfo })); +jest.mock('../../../src/connectors/pancakeswap-sol/clmm-routes/poolInfo', () => ({ + getPoolInfo: pancakeswapSolGetPoolInfo, +})); + +import { poolsRoute } from '../../../src/trading/clmm/pools'; +import { fastifyWithTypeProvider } from '../../utils/testUtils'; + +const POOL = 'Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE'; + +const SAMPLE_POOL_INFO = { + address: POOL, + baseTokenAddress: 'So11111111111111111111111111111111111111112', + quoteTokenAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + feePct: 0.04, + price: 75.6, + baseTokenAmount: 1, + quoteTokenAmount: 2, + activeBinId: -25813, +}; + +const buildApp = async () => { + const server = fastifyWithTypeProvider(); + await server.register(require('@fastify/sensible')); + await server.register(poolsRoute, { prefix: '/trading/clmm' }); + return server; +}; + +describe('Unified CLMM pool-info binCount passthrough', () => { + let app: any; + + beforeAll(async () => { + app = await buildApp(); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + for (const m of [ + orcaGetPoolInfo, + raydiumGetPoolInfo, + uniswapGetPoolInfo, + pancakeswapGetPoolInfo, + meteoraGetPoolInfo, + pancakeswapSolGetPoolInfo, + ]) { + m.mockResolvedValue(SAMPLE_POOL_INFO); + } + }); + + const call = (connector: string, chainNetwork: string, binCount?: number) => + app.inject({ + method: 'GET', + url: '/trading/clmm/pool-info', + query: { + connector, + chainNetwork, + poolAddress: POOL, + ...(binCount === undefined ? {} : { binCount: String(binCount) }), + }, + }); + + it.each([ + ['orca', 'solana-mainnet-beta', () => orcaGetPoolInfo], + ['raydium', 'solana-mainnet-beta', () => raydiumGetPoolInfo], + ['uniswap', 'ethereum-mainnet', () => uniswapGetPoolInfo], + ['pancakeswap', 'ethereum-bsc', () => pancakeswapGetPoolInfo], + ])('forwards binCount to %s', async (connector, chainNetwork, getMock) => { + const response = await call(connector, chainNetwork, 11); + + expect(response.statusCode).toBe(200); + expect(getMock()).toHaveBeenCalledWith(expect.anything(), expect.any(String), POOL, 11); + }); + + it('defaults binCount to 0 when the caller omits it', async () => { + const response = await call('orca', 'solana-mainnet-beta'); + + expect(response.statusCode).toBe(200); + expect(orcaGetPoolInfo).toHaveBeenCalledWith(expect.anything(), expect.any(String), POOL, 0); + }); + + it('does not pass binCount to meteora, which always returns its bins', async () => { + const response = await call('meteora', 'solana-mainnet-beta', 11); + + expect(response.statusCode).toBe(200); + expect(meteoraGetPoolInfo).toHaveBeenCalledWith(expect.anything(), expect.any(String), POOL); + }); + + it('returns the bins the connector produced', async () => { + const bins = [{ binId: -25813, price: 75.6, baseTokenAmount: 1, quoteTokenAmount: 2 }]; + orcaGetPoolInfo.mockResolvedValue({ ...SAMPLE_POOL_INFO, bins }); + + const response = await call('orca', 'solana-mainnet-beta', 1); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.body).bins).toEqual(bins); + }); + + it('rejects a binCount above the supported maximum', async () => { + const response = await call('orca', 'solana-mainnet-beta', 500); + + expect(response.statusCode).toBe(400); + expect(orcaGetPoolInfo).not.toHaveBeenCalled(); + }); +}); From 4e7233b5a0c491ba9ebf91ec47baf88f616d713d Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 09:56:21 -0700 Subject: [PATCH 08/31] fix(bsc): default nodeURL to bsc-dataseed.bnbchain.org MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/templates/chains/ethereum/bsc.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/chains/ethereum/bsc.yml b/src/templates/chains/ethereum/bsc.yml index 567f5157b9..be5392b1c6 100644 --- a/src/templates/chains/ethereum/bsc.yml +++ b/src/templates/chains/ethereum/bsc.yml @@ -1,6 +1,6 @@ chainID: 56 geckoId: bsc -nodeURL: https://binance.llamarpc.com +nodeURL: https://bsc-dataseed.bnbchain.org nativeCurrencySymbol: BNB transactionExecutionTimeoutMs: 10000 # Timeout for waiting for transaction execution (in milliseconds) swapProvider: pancakeswap/router From 0a54fedfe9370b96c1245d90124119d5ca2a4191 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 10:16:57 -0700 Subject: [PATCH 09/31] fix(ethereum): default mainnet nodeURL to Alchemy's public endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/templates/chains/ethereum/mainnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/templates/chains/ethereum/mainnet.yml b/src/templates/chains/ethereum/mainnet.yml index 1e0677a034..86b848df4c 100644 --- a/src/templates/chains/ethereum/mainnet.yml +++ b/src/templates/chains/ethereum/mainnet.yml @@ -1,5 +1,5 @@ chainID: 1 -nodeURL: https://eth.llamarpc.com +nodeURL: https://eth-mainnet.g.alchemy.com/public nativeCurrencySymbol: ETH geckoId: eth transactionExecutionTimeoutMs: 10000 # Timeout for waiting for transaction execution (in milliseconds) From 6aeeb7eae55567253d53a3ab48e9dbed1b008608 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Thu, 13 Aug 2026 11:02:16 -0700 Subject: [PATCH 10/31] docs: cover the poll and CLMM pool-info contracts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/retry-architecture.md | 42 +++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/docs/retry-architecture.md b/docs/retry-architecture.md index 456cea1a57..e2b814af05 100644 --- a/docs/retry-architecture.md +++ b/docs/retry-architecture.md @@ -1,6 +1,10 @@ # LP Close Retry Architecture -**Origin:** [gateway#678](https://github.com/hummingbot/gateway/issues/678) — an Orca LP close failed with Whirlpool error `6018` (`TokenMinSubceeded`, misreported as `MATH_OVERFLOW`), the LP executor went terminally `FAILED` with the position still open on-chain, and subsequent stop requests returned 404. Fixing it properly required deciding **where retries live** across Gateway, the Hummingbot connector, the LP executor, the controller, and hummingbot-api — and auditing every layer against that model. This document describes the issues found and the architecture that now stands, spanning four repositories (gateway, hummingbot, hummingbot-api, condor). +**Origin:** [gateway#678](https://github.com/hummingbot/gateway/issues/678) — an Orca LP close failed with Whirlpool error `6018` (`TokenMinSubceeded`, misreported as `MATH_OVERFLOW`), the LP executor went terminally `FAILED` with the position still open on-chain, and subsequent stop requests returned 404. Fixing it properly required deciding **where retries live** across Gateway, the Hummingbot connector, the LP executor, the controller, and hummingbot-api — and auditing every layer against that model. + +Auditing those layers turned up defects on the paths the LP flow depends on but that #678 never named: the transaction-status contract the poller reads, and the CLMM pool-info contract the UI and agents read. Both are documented here because they share the same root cause as #678 — a caller could not tell a definitive answer from a transient one, or could not ask for what it needed at all. + +This document describes the issues found and the architecture that now stands, spanning five repositories: [gateway#679](https://github.com/hummingbot/gateway/pull/679), [hummingbot#8424](https://github.com/hummingbot/hummingbot/pull/8424), [hummingbot-api#217](https://github.com/hummingbot/hummingbot-api/pull/217), [hummingbot-api-client#25](https://github.com/hummingbot/hummingbot-api-client/pull/25), [condor#204](https://github.com/hummingbot/condor/pull/204). --- @@ -74,6 +78,16 @@ The #678 investigation and the adversarial review of the fix surfaced the follow 15. **Failures were invisible to agents.** The condor tick prompt listed only `RUNNING` executors, so a terminal executor with a live position *vanished from view*. The provider now surfaces orphaned executors with a warning, and `manage_executors` gained `orphaned` and `resolve_orphan` actions. 16. **Orphans had no durable record or resolution path.** The persisted final state now carries the orphan shape; `GET /executors/positions/orphaned` lists candidates (involuntary holds, legacy `FAILED`-with-position, and `SYSTEM_CLEANUP` LP executors from an API restart — the latter flagged `needs_onchain_reconciliation` since no final state was persisted); `POST /executors/{id}/resolve-orphan` marks a recovered position so it stops surfacing. +### CLMM pool-info + +The same audit covered the read path that agents and the dashboard use to see a pool before opening a position. + +17. **The bin distribution was unreachable through the unified route.** `GET /trading/clmm/pool-info` — the route hummingbot-api and condor read through — had no `binCount` in its querystring schema and called every connector as `(fastify, network, poolAddress)`, dropping the parameter orca, raydium and uniswap already implemented. Meteora returns its bins unconditionally, so the gap looked like "only Meteora has bins" rather than "nobody can ask". The route now accepts `binCount` and forwards it, and the parameter is threaded through hummingbot-api, the API client (1.5.8) and condor. +18. **PancakeSwap had no bin support at all.** Added, reusing the V3 tick walk. The two SDKs disagree on numeric type — `@uniswap/v3-sdk` is JSBI-based, `@pancakeswap/v3-sdk` uses native `bigint` — so the walk moved to a shared `clmm-v3-utils` helper that works in `bigint`, with each connector adapting its own SDK rather than one importing the other's math. +19. **PancakeSwap reported virtual liquidity as token amounts.** Both `baseTokenAmount` and `quoteTokenAmount` came from `pool.liquidity` — V3's active liquidity in sqrt-price space, not a token quantity — scaled by each token's decimals, so the same meaningless figure was reported for both sides. Now ERC20 `balanceOf` on the pool contract, the fix Uniswap already carried. +20. **Raydium bypassed Gateway entirely.** hummingbot-api's pool-info special-cased Raydium: it skipped Gateway, called `api-v3.raydium.io` directly, and reshaped that response to imitate Gateway's. The transform hardcoded `active_bin_id` to `None`, `bin_step` to `1` and `bins` to `[]`, so Raydium silently returned degraded data and could not answer `binCount` at all. It now takes the same path as every other CLMM connector. +21. **Two default RPC endpoints were dead.** `eth.llamarpc.com` and `binance.llamarpc.com` answer nothing; Gateway logged "Unable to fetch block number" at startup and every read on those chains failed — which is how a PancakeSwap pool that plainly exists reported "Pool not found". Defaults are now `eth-mainnet.g.alchemy.com/public` and `bsc-dataseed.bnbchain.org`. + --- ## 3. The architecture @@ -189,6 +203,32 @@ Both chains' `/poll` routes share one `TransactionStatusCode` contract; the conn Transient poll errors (RPC failures) report `PENDING`, never `NOT_FOUND` — an unknown outcome is a reason to poll again, not to give up. Note the LP flow's writes do not depend on this poller to progress: the connector's operation calls only return a signature after in-request confirmation. The bound matters for flows that record a hash at submission time, and for the RPC cost of stuck orders. +### 3.6 CLMM pool-info and bins + +One route answers for every CLMM connector, and every caller reaches it through the same chain: + +```mermaid +flowchart LR + A["condor
manage_gateway_clmm(bin_count=N)"] --> B["hummingbot-api-client 1.5.8
get_pool_info(bin_count=N)"] + B --> C["hummingbot-api
GET /gateway/clmm/pool-info?bin_count=N"] + C --> D["gateway
GET /trading/clmm/pool-info?binCount=N"] + D --> E["connector pool-info
bins computed only when asked"] +``` + +`binCount` is a **request for work, not a formatting flag** — the bins are read from chain state, so the default of `0` exists to keep the common call cheap: + +| Connector | Bins | Cost of `binCount = N` | +|---|---|---| +| Meteora | Always returned; ignores `binCount` | Already paid — the DLMM pool exposes its bins directly | +| Orca | On request | Roughly flat: one position fetch, then local math | +| Raydium | On request | Roughly flat: tick-array fetch, then local math | +| Uniswap | On request | Linear: `N + 1` parallel `pool.ticks()` eth_calls | +| PancakeSwap | On request | Linear: `N + 1` parallel `pool.ticks()` eth_calls | + +Gateway caps `binCount` at 401. Because the EVM cost scales with `N` while the Solana cost does not, callers that render a depth column should treat the window size as a parameter rather than a constant. + +The output shape mirrors Meteora's `bins[]` for every connector — `{ binId, price, baseTokenAmount, quoteTokenAmount }` — and is centred on the active tick: bins below the active price hold only quote, bins above hold only base, and the one bin straddling it holds both. + --- ## 4. Accepted residual risks From 80c1445595b9a8fa4f36a4d7e5839a742a03f556 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 16:07:19 -0700 Subject: [PATCH 11/31] fix(clmm): validate open amounts at the route, matching add 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/trading/trading-clmm-routes/open.ts | 8 ++++++++ test/trading/clmm/routes.test.ts | 3 +++ 2 files changed, 11 insertions(+) diff --git a/src/trading/trading-clmm-routes/open.ts b/src/trading/trading-clmm-routes/open.ts index a2739b0524..934ef289af 100644 --- a/src/trading/trading-clmm-routes/open.ts +++ b/src/trading/trading-clmm-routes/open.ts @@ -144,6 +144,14 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { // Parse chain and network from chainNetwork parameter const { network } = parseChainNetwork(chainNetwork); + // Same contract as add.ts: single-sided opens are valid, but at least one + // side must be positive — reject here rather than deep in connector code. + const baseAmount = baseTokenAmount ?? 0; + const quoteAmount = quoteTokenAmount ?? 0; + if (baseAmount <= 0 && quoteAmount <= 0) { + throw httpErrors.badRequest('At least one of baseTokenAmount or quoteTokenAmount must be greater than 0'); + } + // Route to appropriate connector switch (connector) { case 'uniswap': diff --git a/test/trading/clmm/routes.test.ts b/test/trading/clmm/routes.test.ts index 081f7b0b5e..8fabe59197 100644 --- a/test/trading/clmm/routes.test.ts +++ b/test/trading/clmm/routes.test.ts @@ -357,6 +357,9 @@ describe('Unified Trading CLMM Routes', () => { lowerPrice: 1800, upperPrice: 2200, poolAddress: '0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8', + // An otherwise-valid body: amount validation runs before connector + // routing, and this test is about the connector rejection. + baseTokenAmount: 1, }, }); From 7eb4ffc839450d926d001c0d91dccf8c4494f284 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 16:21:35 -0700 Subject: [PATCH 12/31] refactor(clmm): drop the never-populated reward fields from PositionInfo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/schemas/clmm-schema.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/schemas/clmm-schema.ts b/src/schemas/clmm-schema.ts index ce1fc94f30..c9fe0775a4 100644 --- a/src/schemas/clmm-schema.ts +++ b/src/schemas/clmm-schema.ts @@ -153,8 +153,6 @@ export const PositionInfoSchema = Type.Object( lowerPrice: Type.Number(), upperPrice: Type.Number(), price: Type.Number(), - rewardTokenAddress: Type.Optional(Type.String()), - rewardAmount: Type.Optional(Type.Number()), }, { $id: 'PositionInfo' }, ); From f686579a7e9931b8714569611fa689f4e6c26f07 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 16:35:49 -0700 Subject: [PATCH 13/31] feat(clmm): binCount bins for pancakeswap-sol; fix inverted out-of-range amounts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- .../pancakeswap-sol/clmm-routes/poolInfo.ts | 30 ++++- .../pancakeswap-sol/pancakeswap-sol.bins.ts | 123 ++++++++++++++++++ .../pancakeswap-sol/pancakeswap-sol.math.ts | 11 +- .../pancakeswap-sol/pancakeswap-sol.ts | 14 +- src/connectors/pancakeswap-sol/schemas.ts | 9 ++ src/trading/clmm/pools.ts | 4 +- 6 files changed, 179 insertions(+), 12 deletions(-) create mode 100644 src/connectors/pancakeswap-sol/pancakeswap-sol.bins.ts diff --git a/src/connectors/pancakeswap-sol/clmm-routes/poolInfo.ts b/src/connectors/pancakeswap-sol/clmm-routes/poolInfo.ts index 581fbf5a45..ead3db316e 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/poolInfo.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/poolInfo.ts @@ -4,9 +4,15 @@ import { Solana } from '../../../chains/solana/solana'; import { GetPoolInfoRequestType, PoolInfo, PoolInfoSchema } from '../../../schemas/clmm-schema'; import { logger } from '../../../services/logger'; import { PancakeswapSol } from '../pancakeswap-sol'; +import { computeBinDistribution } from '../pancakeswap-sol.bins'; import { PancakeswapSolClmmGetPoolInfoRequest } from '../schemas'; -export async function getPoolInfo(fastify: FastifyInstance, network: string, poolAddress: string): Promise { +export async function getPoolInfo( + fastify: FastifyInstance, + network: string, + poolAddress: string, + binCount: number = 0, +): Promise { const pancakeswap = await PancakeswapSol.getInstance(network); if (!poolAddress) { @@ -19,6 +25,24 @@ export async function getPoolInfo(fastify: FastifyInstance, network: string, poo throw fastify.httpErrors.notFound(`Pool not found: ${poolAddress}`); } + // Optionally include the per-bin distribution around the current tick — only + // fires the extra tick-array fetch when binCount > 0 so default latency is + // unchanged (same contract as orca/raydium). + if (binCount > 0) { + const solana = await Solana.getInstance(network); + poolInfo.bins = await computeBinDistribution({ + connection: solana.connection, + poolAddress, + tickSpacing: poolInfo.binStep as number, + currentTick: poolInfo.activeBinId as number, + currentPrice: poolInfo.price, + liquidity: (poolInfo as any)._liquidity as bigint, + decimals0: (poolInfo as any)._mintDecimals0 as number, + decimals1: (poolInfo as any)._mintDecimals1 as number, + binCount, + }); + } + return poolInfo; } @@ -40,8 +64,8 @@ export const poolInfoRoute: FastifyPluginAsync = async (fastify) => { }, async (request): Promise => { try { - const { network = 'mainnet-beta', poolAddress } = request.query; - return await getPoolInfo(fastify, network, poolAddress); + const { network = 'mainnet-beta', poolAddress, binCount = 0 } = request.query; + return await getPoolInfo(fastify, network, poolAddress, binCount); } catch (e: any) { logger.error('Pool info error:', e); // Re-throw httpErrors as-is diff --git a/src/connectors/pancakeswap-sol/pancakeswap-sol.bins.ts b/src/connectors/pancakeswap-sol/pancakeswap-sol.bins.ts new file mode 100644 index 0000000000..d2b5a881ff --- /dev/null +++ b/src/connectors/pancakeswap-sol/pancakeswap-sol.bins.ts @@ -0,0 +1,123 @@ +import { Connection, PublicKey } from '@solana/web3.js'; +import BN from 'bn.js'; + +import { BinLiquidity } from '../../schemas/clmm-schema'; +import { logger } from '../../services/logger'; + +import { getAmountsFromLiquidity } from './pancakeswap-sol.math'; +import { getTickArrayAddress, getTickArrayStartIndexFromTick, tickToPrice } from './pancakeswap-sol.parser'; + +// TickArrayState layout (idl/clmm.json — identical to Raydium CLMM, which this +// program forks): discriminator(8) + pool_id(32) + start_tick_index(i32, 4) + +// ticks[60 x TickState] + initialized_tick_count(u8) + recent_epoch(u64) + padding. +// TickState: tick(i32, 4) + liquidity_net(i128, 16) + liquidity_gross(u128, 16) + +// fee_growth_outside_0_x64(16) + fee_growth_outside_1_x64(16) + +// reward_growths_outside_x64(3 x 16) + padding(13 x u32, 52) = 168 bytes. +const TICK_ARRAY_HEADER = 8 + 32 + 4; +const TICK_STATE_SIZE = 168; + +function readI128LE(data: Buffer, offset: number): bigint { + let value = 0n; + for (let i = 0; i < 16; i++) { + value += BigInt(data[offset + i]) << BigInt(8 * i); + } + if (value >= 1n << 127n) { + value -= 1n << 128n; + } + return value; +} + +function liquidityNetAt(arrays: Map, tickIndex: number, tickSpacing: number): bigint { + const startIndex = getTickArrayStartIndexFromTick(tickIndex, tickSpacing); + const data = arrays.get(startIndex); + if (!data) return 0n; + const slot = Math.floor((tickIndex - startIndex) / tickSpacing); + const base = TICK_ARRAY_HEADER + slot * TICK_STATE_SIZE; + if (base + TICK_STATE_SIZE > data.length) return 0n; + // A zeroed (uninitialized) slot fails the tick check; its liquidity_net would + // be 0 anyway, so either way the boundary contributes nothing. + if (data.readInt32LE(base) !== tickIndex) return 0n; + return readI128LE(data, base + 4); +} + +/** + * Per-bin liquidity distribution around the current tick, mirroring the + * raydium/orca bin walk: propagate active liquidity outward across initialized + * tick boundaries, then convert each bin's L into token amounts. + */ +export async function computeBinDistribution(args: { + connection: Connection; + poolAddress: string; + tickSpacing: number; + currentTick: number; + currentPrice: number; // human units (quote per base) + liquidity: bigint; // active liquidity at the current tick + decimals0: number; + decimals1: number; + binCount: number; +}): Promise { + const { connection, poolAddress, tickSpacing, currentTick, currentPrice, liquidity, decimals0, decimals1, binCount } = + args; + if (binCount <= 0) return []; + + // Snap the bin grid so the current tick lands inside a bin. + const halfBins = Math.floor(binCount / 2); + const snapped = Math.floor(currentTick / tickSpacing) * tickSpacing; + const firstBinStart = snapped - halfBins * tickSpacing; + const boundaries: number[] = []; + for (let i = 0; i <= binCount; i++) { + boundaries.push(firstBinStart + i * tickSpacing); + } + + // Fetch every tick array the boundaries touch in one RPC round-trip. + const poolPubkey = new PublicKey(poolAddress); + const startIndexes = [...new Set(boundaries.map((t) => getTickArrayStartIndexFromTick(t, tickSpacing)))]; + const arrays = new Map(); + try { + const accounts = await connection.getMultipleAccountsInfo( + startIndexes.map((s) => getTickArrayAddress(poolPubkey, s)), + ); + accounts.forEach((account, i) => { + if (account) arrays.set(startIndexes[i], account.data); + }); + } catch (e) { + logger.warn(`pancakeswap-sol bin fetch failed for ${poolAddress}: ${e}`); + return []; + } + + // Propagate L outward from the current bin across boundary liquidity_net. + const curIdx = Math.floor((currentTick - firstBinStart) / tickSpacing); + const binLs: bigint[] = new Array(binCount); + binLs[curIdx] = liquidity; + for (let i = curIdx + 1; i < binCount; i++) { + binLs[i] = binLs[i - 1] + liquidityNetAt(arrays, boundaries[i], tickSpacing); + } + for (let i = curIdx - 1; i >= 0; i--) { + binLs[i] = binLs[i + 1] - liquidityNetAt(arrays, boundaries[i + 1], tickSpacing); + } + + const decimalDiff = decimals0 - decimals1; + const bins: BinLiquidity[] = []; + for (let i = 0; i < binCount; i++) { + const tickStart = boundaries[i]; + const priceLower = tickToPrice(tickStart, decimalDiff); + const priceUpper = tickToPrice(boundaries[i + 1], decimalDiff); + let baseTokenAmount = 0; + let quoteTokenAmount = 0; + const L = binLs[i]; + if (L > 0n) { + const amounts = getAmountsFromLiquidity( + currentPrice, + priceLower, + priceUpper, + new BN(L.toString()), + decimals0, + decimals1, + ); + baseTokenAmount = amounts.amount0; + quoteTokenAmount = amounts.amount1; + } + bins.push({ binId: tickStart, price: priceLower, baseTokenAmount, quoteTokenAmount }); + } + return bins; +} diff --git a/src/connectors/pancakeswap-sol/pancakeswap-sol.math.ts b/src/connectors/pancakeswap-sol/pancakeswap-sol.math.ts index 3150e800fb..ef9d2cfe29 100644 --- a/src/connectors/pancakeswap-sol/pancakeswap-sol.math.ts +++ b/src/connectors/pancakeswap-sol/pancakeswap-sol.math.ts @@ -106,13 +106,14 @@ export function getAmountsFromLiquidity( let amount1Raw: number; if (currentPrice < lowerPrice) { - // Price below range - all liquidity in token1 - amount0Raw = 0; - amount1Raw = liquidityNum * (sqrtPriceUpper - sqrtPriceLower); - } else if (currentPrice >= upperPrice) { - // Price above range - all liquidity in token0 + // Price below range - all liquidity in token0: the range sits above the + // market, so the pool sells token0 as price rises through it. amount0Raw = (liquidityNum * (sqrtPriceUpper - sqrtPriceLower)) / (sqrtPriceLower * sqrtPriceUpper); amount1Raw = 0; + } else if (currentPrice >= upperPrice) { + // Price above range - all liquidity in token1 (token0 fully sold on the way up) + amount0Raw = 0; + amount1Raw = liquidityNum * (sqrtPriceUpper - sqrtPriceLower); } else { // Price in range amount0Raw = (liquidityNum * (sqrtPriceUpper - sqrtPriceCurrent)) / (sqrtPriceCurrent * sqrtPriceUpper); diff --git a/src/connectors/pancakeswap-sol/pancakeswap-sol.ts b/src/connectors/pancakeswap-sol/pancakeswap-sol.ts index acf01502dc..e233821395 100644 --- a/src/connectors/pancakeswap-sol/pancakeswap-sol.ts +++ b/src/connectors/pancakeswap-sol/pancakeswap-sol.ts @@ -145,8 +145,13 @@ export class PancakeswapSol { const tickSpacing = data.readUInt16LE(offset); offset += 2; - // Read liquidity (16 bytes, u128) - not currently used but part of data structure - // const liquidity = data.readBigUInt64LE(offset); + // Read liquidity (16 bytes, u128) - the active liquidity at the current tick, + // needed for the bin distribution walk + const liquidityBytes = data.slice(offset, offset + 16); + let liquidityValue = BigInt(0); + for (let i = 0; i < 16; i++) { + liquidityValue += BigInt(liquidityBytes[i]) << BigInt(i * 8); + } offset += 16; // Read sqrt_price_x64 (16 bytes, u128) @@ -260,6 +265,11 @@ export class PancakeswapSol { (poolInfo as any)._feeGrowthGlobal0 = feeGrowthGlobal0; (poolInfo as any)._feeGrowthGlobal1 = feeGrowthGlobal1; (poolInfo as any)._rewardGrowthGlobalX64 = rewardGrowthGlobalX64; + // Raw pool state for the bin distribution walk (poolInfo.binStep/activeBinId + // carry tickSpacing/tickCurrent already; these carry the rest) + (poolInfo as any)._liquidity = liquidityValue; + (poolInfo as any)._mintDecimals0 = mintDecimals0; + (poolInfo as any)._mintDecimals1 = mintDecimals1; return poolInfo; } catch (error) { diff --git a/src/connectors/pancakeswap-sol/schemas.ts b/src/connectors/pancakeswap-sol/schemas.ts index bcf2b1924a..25d919df9d 100644 --- a/src/connectors/pancakeswap-sol/schemas.ts +++ b/src/connectors/pancakeswap-sol/schemas.ts @@ -31,6 +31,15 @@ export const PancakeswapSolClmmGetPoolInfoRequest = Type.Object({ description: 'PancakeSwap CLMM pool address', examples: [CLMM_POOL_ADDRESS_EXAMPLE], }), + binCount: Type.Optional( + Type.Integer({ + description: + 'If > 0, include a `bins` array of per-tick liquidity around the active tick. Default 0 = skip the bin fetch.', + default: 0, + minimum: 0, + maximum: 401, + }), + ), }); export type PancakeswapSolClmmGetPoolInfoRequestType = Static; diff --git a/src/trading/clmm/pools.ts b/src/trading/clmm/pools.ts index c6c8c8d199..485890aea7 100644 --- a/src/trading/clmm/pools.ts +++ b/src/trading/clmm/pools.ts @@ -38,7 +38,7 @@ const UnifiedPoolInfoRequestSchema = Type.Object({ Type.Integer({ description: 'If > 0, include a `bins` array of per-tick liquidity around the active tick. Supported by ' + - 'orca, raydium, uniswap and pancakeswap; Meteora always returns its bins and ignores this. ' + + 'every connector except Meteora, which always returns its bins and ignores this. ' + 'Default 0 = skip the bin fetch.', default: 0, minimum: 0, @@ -86,7 +86,7 @@ async function getSolanaPoolInfo( // Meteora always returns its bins; it has no binCount parameter. return await meteoraGetPoolInfo(fastify, network, poolAddress); case 'pancakeswap-sol': - return await pancakeswapSolGetPoolInfo(fastify, network, poolAddress); + return await pancakeswapSolGetPoolInfo(fastify, network, poolAddress, binCount); case 'orca': return await orcaGetPoolInfo(fastify, network, poolAddress, binCount); default: From 227f22ded1f15ebd2ff38deb836a517bea718dd8 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 16:48:11 -0700 Subject: [PATCH 14/31] refactor(clmm): one fee-tier vocabulary for create-pool; canonical schemas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- .../meteora/clmm-routes/createPool.ts | 4 +- src/connectors/orca/clmm-routes/createPool.ts | 5 +- .../pancakeswap-sol/clmm-routes/createPool.ts | 69 +++++++------ src/connectors/pancakeswap-sol/schemas.ts | 17 ++-- .../pancakeswap/clmm-routes/createPool.ts | 4 +- .../raydium/clmm-routes/createPool.ts | 5 +- .../uniswap/clmm-routes/createPool.ts | 4 +- src/schemas/clmm-schema.ts | 66 +++++++++++++ .../trading-clmm-routes/create-pool.ts | 99 +++++++------------ 9 files changed, 159 insertions(+), 114 deletions(-) diff --git a/src/connectors/meteora/clmm-routes/createPool.ts b/src/connectors/meteora/clmm-routes/createPool.ts index c301d9bc46..3f99ac0269 100644 --- a/src/connectors/meteora/clmm-routes/createPool.ts +++ b/src/connectors/meteora/clmm-routes/createPool.ts @@ -9,7 +9,7 @@ import BN from 'bn.js'; import { FastifyPluginAsync } from 'fastify'; import { Solana } from '../../../chains/solana/solana'; -import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/amm-schema'; +import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { sanitizeErrorMessage } from '../../../services/sanitize'; @@ -208,8 +208,6 @@ export async function createPool( price: seedPrice, data: { fee: txData.meta.fee / 1e9, - baseTokenAmountAdded: 0, - quoteTokenAmountAdded: 0, }, }; } diff --git a/src/connectors/orca/clmm-routes/createPool.ts b/src/connectors/orca/clmm-routes/createPool.ts index e2a06b0338..f44fd8ba48 100644 --- a/src/connectors/orca/clmm-routes/createPool.ts +++ b/src/connectors/orca/clmm-routes/createPool.ts @@ -5,7 +5,7 @@ import { Keypair, PublicKey } from '@solana/web3.js'; import { FastifyPluginAsync } from 'fastify'; import { Solana } from '../../../chains/solana/solana'; -import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/amm-schema'; +import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { sanitizeErrorMessage } from '../../../services/sanitize'; @@ -157,9 +157,6 @@ export async function createPool( price: seedPrice, data: { fee: txData.meta.fee / 1e9, - // Pool created + initialized only — no liquidity/position seeded. - baseTokenAmountAdded: 0, - quoteTokenAmountAdded: 0, }, }; } diff --git a/src/connectors/pancakeswap-sol/clmm-routes/createPool.ts b/src/connectors/pancakeswap-sol/clmm-routes/createPool.ts index 6660c7eb5f..b9740a6579 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/createPool.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/createPool.ts @@ -5,7 +5,7 @@ import BN from 'bn.js'; import { FastifyPluginAsync } from 'fastify'; import { Solana } from '../../../chains/solana/solana'; -import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/amm-schema'; +import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { sanitizeErrorMessage } from '../../../services/sanitize'; @@ -67,12 +67,44 @@ async function fetchMarketPrice(network: string, baseToken: string, quoteToken: return quote.amountOut / quote.amountIn; // quote token per base token } +/** + * Derive the amm_config PDA for a fee-config index. The program (a Raydium CLMM + * fork) seeds it with ["amm_config", index] — index encoded big-endian, the same + * Raydium convention this connector's tick_array PDAs use. The little-endian + * (plain Anchor/borsh) form is tried as a fallback before failing. + */ +async function resolveAmmConfigByIndex(solana: Solana, ammConfigIndex: number): Promise { + if (!Number.isInteger(ammConfigIndex) || ammConfigIndex < 0 || ammConfigIndex > 0xffff) { + throw httpErrors.badRequest(`ammConfigIndex must be an integer in [0, 65535], got ${ammConfigIndex}`); + } + const candidates: PublicKey[] = []; + for (const endian of ['BE', 'LE'] as const) { + const indexBuffer = Buffer.alloc(2); + if (endian === 'BE') indexBuffer.writeUInt16BE(ammConfigIndex, 0); + else indexBuffer.writeUInt16LE(ammConfigIndex, 0); + const [pda] = PublicKey.findProgramAddressSync( + [Buffer.from('amm_config'), indexBuffer], + PANCAKESWAP_CLMM_PROGRAM_ID, + ); + candidates.push(pda); + } + for (const pda of candidates) { + const info = await solana.connection.getAccountInfo(pda); + if (info && info.owner.equals(PANCAKESWAP_CLMM_PROGRAM_ID)) { + return pda; + } + } + throw httpErrors.badRequest( + `No amm_config account found on-chain for index ${ammConfigIndex} ` + + `(tried ${candidates.map((c) => c.toBase58()).join(', ')})`, + ); +} + /** * Create and initialize (but do NOT seed a position for) a PancakeSwap Solana CLMM pool. * - * @param ammConfig Base58 address of an existing on-chain amm_config account for the desired fee tier. - * Required — there is no API to enumerate amm_config accounts, so the caller supplies - * the config for the fee tier they want (mirrors Meteora DAMM v2 configAddress). + * @param ammConfigIndex Fee-config index; resolves to the program's amm_config PDA + * (["amm_config", index]) and is validated on-chain. Default 0. */ export async function createPool( network: string, @@ -80,31 +112,13 @@ export async function createPool( baseToken: string, quoteToken: string, initialPrice?: number, - ammConfig?: string, + ammConfigIndex: number = 0, ): Promise { const solana = await Solana.getInstance(network); // Ensure the connector singleton is initialized (mirrors the other pancakeswap-sol routes). await PancakeswapSol.getInstance(network); - // Validate the required amm_config address and confirm it exists on-chain. - if (!ammConfig) { - throw httpErrors.badRequest('ammConfig is required: pass the address of an existing on-chain amm_config account'); - } - let ammConfigPubkey: PublicKey; - try { - ammConfigPubkey = new PublicKey(ammConfig); - } catch { - throw httpErrors.badRequest(sanitizeErrorMessage('Invalid ammConfig address: {}', ammConfig)); - } - const ammConfigInfo = await solana.connection.getAccountInfo(ammConfigPubkey); - if (!ammConfigInfo) { - throw httpErrors.badRequest(`amm_config account not found: ${ammConfigPubkey.toBase58()}`); - } - if (!ammConfigInfo.owner.equals(PANCAKESWAP_CLMM_PROGRAM_ID)) { - throw httpErrors.badRequest( - `amm_config ${ammConfigPubkey.toBase58()} is not owned by the PancakeSwap CLMM program`, - ); - } + const ammConfigPubkey = await resolveAmmConfigByIndex(solana, ammConfigIndex); // Resolve mints, decimals and token programs from authoritative on-chain data. const baseMint = await resolveMint(solana, baseToken); @@ -205,9 +219,6 @@ export async function createPool( price: seedPrice, data: { fee: txData.meta.fee / 1e9, - // Pool created + initialized only — no liquidity/position seeded. - baseTokenAmountAdded: 0, - quoteTokenAmountAdded: 0, }, }; } @@ -240,9 +251,9 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { baseToken, quoteToken, initialPrice, - ammConfig, + ammConfigIndex, } = request.body; - return await createPool(network, walletAddress!, baseToken, quoteToken, initialPrice, ammConfig); + return await createPool(network, walletAddress!, baseToken, quoteToken, initialPrice, ammConfigIndex); } catch (e: any) { logger.error('Create pool error:', e); if (e.statusCode) throw e; diff --git a/src/connectors/pancakeswap-sol/schemas.ts b/src/connectors/pancakeswap-sol/schemas.ts index 25d919df9d..84a08759f0 100644 --- a/src/connectors/pancakeswap-sol/schemas.ts +++ b/src/connectors/pancakeswap-sol/schemas.ts @@ -127,13 +127,16 @@ export const PancakeswapSolClmmCreatePoolRequest = Type.Object({ examples: [QUOTE_TOKEN_AMOUNT / BASE_TOKEN_AMOUNT], }), ), - ammConfig: Type.String({ - description: - 'Base58 address of an existing on-chain amm_config account for the desired fee tier. ' + - 'There is no API to enumerate amm_config accounts, so this must be supplied explicitly. ' + - 'The pool_state PDA is derived from this config plus the (canonically ordered) token mints.', - examples: ['E64NGkDLLCdQ2yFNPcavaKptrEgmiQaNykUuLC1Qgwyp'], - }), + ammConfigIndex: Type.Optional( + Type.Integer({ + description: + 'Fee-config index; resolves to the amm_config PDA (["amm_config", index]) and is validated ' + + 'on-chain. Each index is a fee tier created by the program admin. Default 0.', + default: 0, + minimum: 0, + maximum: 65535, + }), + ), }); export type PancakeswapSolClmmCreatePoolRequestType = Static; diff --git a/src/connectors/pancakeswap/clmm-routes/createPool.ts b/src/connectors/pancakeswap/clmm-routes/createPool.ts index 85e8bda09c..a135888c0c 100644 --- a/src/connectors/pancakeswap/clmm-routes/createPool.ts +++ b/src/connectors/pancakeswap/clmm-routes/createPool.ts @@ -7,7 +7,7 @@ import { FastifyPluginAsync } from 'fastify'; import JSBI from 'jsbi'; import { Ethereum, TokenInfo } from '../../../chains/ethereum/ethereum'; -import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/amm-schema'; +import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { @@ -200,8 +200,6 @@ export async function createPool( price: seedPrice, data: { fee: gasFee, - baseTokenAmountAdded: 0, // create-pool only initializes price; no liquidity is seeded - quoteTokenAmountAdded: 0, }, }; } diff --git a/src/connectors/raydium/clmm-routes/createPool.ts b/src/connectors/raydium/clmm-routes/createPool.ts index c6d4ed0344..919dd73689 100644 --- a/src/connectors/raydium/clmm-routes/createPool.ts +++ b/src/connectors/raydium/clmm-routes/createPool.ts @@ -12,7 +12,7 @@ import { Decimal } from 'decimal.js'; import { FastifyPluginAsync } from 'fastify'; import { Solana } from '../../../chains/solana/solana'; -import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/amm-schema'; +import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { sanitizeErrorMessage } from '../../../services/sanitize'; @@ -197,9 +197,6 @@ export async function createPool( price: seedPrice, data: { fee: txData.meta.fee / 1e9, - // Pool created + initialized only — no liquidity/position seeded. - baseTokenAmountAdded: 0, - quoteTokenAmountAdded: 0, }, }; } diff --git a/src/connectors/uniswap/clmm-routes/createPool.ts b/src/connectors/uniswap/clmm-routes/createPool.ts index 7ef1320b2d..1d69b4d0dd 100644 --- a/src/connectors/uniswap/clmm-routes/createPool.ts +++ b/src/connectors/uniswap/clmm-routes/createPool.ts @@ -7,7 +7,7 @@ import { FastifyPluginAsync } from 'fastify'; import JSBI from 'jsbi'; import { Ethereum, TokenInfo } from '../../../chains/ethereum/ethereum'; -import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/amm-schema'; +import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { UniswapClmmCreatePoolRequest } from '../schemas'; @@ -202,8 +202,6 @@ export async function createPool( price: seedPrice, data: { fee: gasFee, - baseTokenAmountAdded: 0, // create-pool only initializes price; no liquidity is seeded - quoteTokenAmountAdded: 0, }, }; } diff --git a/src/schemas/clmm-schema.ts b/src/schemas/clmm-schema.ts index c9fe0775a4..23f10f09b0 100644 --- a/src/schemas/clmm-schema.ts +++ b/src/schemas/clmm-schema.ts @@ -322,6 +322,72 @@ export const ClosePositionResponse = Type.Object( ); export type ClosePositionResponseType = Static; +// ======================================== +// CLMM Create Pool Types +// ======================================== + +// One fee-tier vocabulary across connectors: binStep is the bin/tick granularity +// (Meteora DLMM bin step, Orca tick spacing), feeBps the base fee in basis points +// (Meteora DLMM base fee; Uniswap/PancakeSwap V3 tier — 1, 5, 30 or 100 bps, +// PancakeSwap also 25), ammConfigIndex the Raydium-family fee-config index +// (Raydium API config list; pancakeswap-sol amm_config PDA index). +export const CreatePoolRequest = Type.Object( + { + network: Type.Optional(Type.String()), + walletAddress: Type.Optional(Type.String()), + baseToken: Type.String(), + quoteToken: Type.String(), + initialPrice: Type.Optional( + Type.Number({ + description: + 'Initial pool price as quote per base. If omitted, the current market price is fetched from the ' + + 'unified swap router so the pool opens on-market.', + }), + ), + binStep: Type.Optional( + Type.Number({ + description: 'Bin/tick granularity: Meteora DLMM bin step (bps); Orca Whirlpool tick spacing.', + }), + ), + feeBps: Type.Optional( + Type.Number({ + description: + 'Base fee in basis points: Meteora DLMM base fee; Uniswap/PancakeSwap V3 fee tier ' + + '(1, 5, 30 or 100 bps; PancakeSwap also 25).', + }), + ), + ammConfigIndex: Type.Optional( + Type.Number({ + description: + 'Fee-config index for the Raydium CLMM family: Raydium API config list index; ' + + 'pancakeswap-sol amm_config PDA index. Default 0.', + }), + ), + }, + { $id: 'ClmmCreatePoolRequest' }, +); +export type CreatePoolRequestType = Static; + +// CLMM create-pool initializes an EMPTY pool — liquidity arrives later via +// open-position — so unlike the AMM response there are no seeded amounts. +export const CreatePoolResponse = Type.Object( + { + signature: Type.String(), + status: Type.Number({ description: 'TransactionStatus enum value' }), + poolAddress: Type.String({ description: 'Address of the newly created pool' }), + price: Type.Optional(Type.Number({ description: 'Initial price the pool was initialized at (quote per base)' })), + + // Only included when status = CONFIRMED + data: Type.Optional( + Type.Object({ + fee: Type.Number(), + }), + ), + }, + { $id: 'ClmmCreatePoolResponse' }, +); +export type CreatePoolResponseType = Static; + export const QuotePositionRequest = Type.Omit(OpenPositionRequest, ['walletAddress'], { $id: 'QuotePositionRequest' }); export type QuotePositionRequestType = Static; diff --git a/src/trading/trading-clmm-routes/create-pool.ts b/src/trading/trading-clmm-routes/create-pool.ts index 9aef970949..5f3ca2913f 100644 --- a/src/trading/trading-clmm-routes/create-pool.ts +++ b/src/trading/trading-clmm-routes/create-pool.ts @@ -9,7 +9,11 @@ import { createPool as pancakeswapCreatePool } from '../../connectors/pancakeswa import { createPool as pancakeswapSolCreatePool } from '../../connectors/pancakeswap-sol/clmm-routes/createPool'; import { createPool as raydiumCreatePool } from '../../connectors/raydium/clmm-routes/createPool'; import { createPool as uniswapCreatePool } from '../../connectors/uniswap/clmm-routes/createPool'; -import { CreatePoolResponse, CreatePoolResponseType } from '../../schemas/amm-schema'; +import { + CreatePoolResponse, + CreatePoolResponseType, + CreatePoolRequest as ClmmCreatePoolRequest, +} from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; @@ -34,41 +38,25 @@ function parseChainNetwork(chainNetwork: string): { chain: string; network: stri // Unified CLMM create-pool. Creates + initializes a pool at an initial price (no position is // seeded — concentrated-liquidity positions need a range, opened separately via open-position). // Per-connector extras are optional and consumed only by their owning connector. -const UnifiedClmmCreatePoolRequest = Type.Object({ - connector: Type.String({ - description: 'CLMM connector name (meteora, raydium, uniswap, orca, pancakeswap, pancakeswap-sol)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), - walletAddress: Type.String({ description: 'Wallet address (pool creator + payer)', default: defaultWallet }), - baseToken: Type.String({ description: 'Base token symbol or address' }), - quoteToken: Type.String({ description: 'Quote token symbol or address' }), - initialPrice: Type.Optional( - Type.Number({ - description: - 'Initial pool price as quote per base. If omitted, the current market price is fetched from the ' + - 'unified swap router so the pool opens on-market.', +// Composed from the canonical ClmmCreatePoolRequest (schemas/clmm-schema.ts): +// the unified route swaps per-connector `network` for connector + chainNetwork +// and defaults the wallet. +const UnifiedClmmCreatePoolRequest = Type.Composite([ + Type.Object({ + connector: Type.String({ + description: 'CLMM connector name (meteora, raydium, uniswap, orca, pancakeswap, pancakeswap-sol)', + default: 'meteora', + examples: ['meteora'], }), - ), - // Connector-specific extras (optional; ignored by connectors that do not use them): - binStep: Type.Optional(Type.Number({ description: 'Meteora DLMM bin step (bps)' })), - feeBps: Type.Optional(Type.Number({ description: 'Meteora DLMM base fee (bps)' })), - ammConfigIndex: Type.Optional(Type.Number({ description: 'Raydium CLMM AMM config index (fee tier)' })), - fee: Type.Optional( - Type.Number({ - description: 'V3 fee tier — Uniswap (100 | 500 | 3000 | 10000) or PancakeSwap (100 | 500 | 2500 | 10000)', + chainNetwork: Type.String({ + description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', + default: 'solana-mainnet-beta', + examples: ['solana-mainnet-beta'], }), - ), - tickSpacing: Type.Optional(Type.Number({ description: 'Orca Whirlpool tick spacing (fee tier)' })), - ammConfig: Type.Optional(Type.String({ description: 'pancakeswap-sol CLMM amm_config account address (required)' })), - gasPrice: Type.Optional(Type.Number({ description: 'EVM gas price in gwei (uniswap/pancakeswap)' })), - maxGas: Type.Optional(Type.Number({ description: 'EVM max gas limit (uniswap/pancakeswap)' })), -}); + walletAddress: Type.String({ description: 'Wallet address (pool creator + payer)', default: defaultWallet }), + }), + Type.Omit(ClmmCreatePoolRequest, ['network', 'walletAddress'], {}), +]); export const createPoolRoute: FastifyPluginAsync = async (fastify) => { fastify.post<{ @@ -97,15 +85,20 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { binStep, feeBps, ammConfigIndex, - fee, - tickSpacing, - ammConfig, - gasPrice, - maxGas, } = request.body; const { network } = parseChainNetwork(chainNetwork); + // EVM V3 fee tiers are denominated in hundredths of a bip; feeBps is the + // route's one fee vocabulary, so map it (1 bps -> 100). + if ((connector === 'uniswap' || connector === 'pancakeswap') && feeBps === undefined) { + throw httpErrors.badRequest( + `feeBps is required for ${connector}: the V3 fee tier in basis points ` + + '(1, 5, 30 or 100; pancakeswap also 25)', + ); + } + const evmFeeTier = feeBps === undefined ? undefined : feeBps * 100; + switch (connector) { case 'meteora': return await meteoraCreatePool( @@ -120,29 +113,13 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { case 'raydium': return await raydiumCreatePool(network, walletAddress, baseToken, quoteToken, initialPrice, ammConfigIndex); case 'uniswap': - return await uniswapCreatePool( - network, - walletAddress, - baseToken, - quoteToken, - initialPrice, - fee, - gasPrice, - maxGas, - ); + return await uniswapCreatePool(network, walletAddress, baseToken, quoteToken, initialPrice, evmFeeTier); case 'orca': - return await orcaCreatePool(network, walletAddress, baseToken, quoteToken, initialPrice, tickSpacing); + // Orca's fee tier IS its tick spacing — binStep is the route's one + // granularity vocabulary. + return await orcaCreatePool(network, walletAddress, baseToken, quoteToken, initialPrice, binStep); case 'pancakeswap': - return await pancakeswapCreatePool( - network, - walletAddress, - baseToken, - quoteToken, - initialPrice, - fee, - gasPrice, - maxGas, - ); + return await pancakeswapCreatePool(network, walletAddress, baseToken, quoteToken, initialPrice, evmFeeTier); case 'pancakeswap-sol': return await pancakeswapSolCreatePool( network, @@ -150,7 +127,7 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { baseToken, quoteToken, initialPrice, - ammConfig, + ammConfigIndex, ); default: throw httpErrors.badRequest(`Unsupported CLMM connector: ${connector}`); From 95e7ecd684612426f54999d72b863c179d793901 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 17:07:27 -0700 Subject: [PATCH 15/31] =?UTF-8?q?refactor(amm):=20consistent=20unified=20p?= =?UTF-8?q?arams=20=E2=80=94=20no=20gas=20overrides,=20one=20config-index?= =?UTF-8?q?=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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, ). - 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- .../pancakeswap/amm-routes/addLiquidity.ts | 16 +-- .../pancakeswap/amm-routes/createPool.ts | 17 +-- .../pancakeswap/amm-routes/removeLiquidity.ts | 24 +--- .../pancakeswap/clmm-routes/createPool.ts | 11 +- src/connectors/pancakeswap/schemas.ts | 110 ------------------ .../uniswap/amm-routes/addLiquidity.ts | 16 +-- .../uniswap/amm-routes/createPool.ts | 17 +-- .../uniswap/amm-routes/removeLiquidity.ts | 24 +--- .../uniswap/clmm-routes/createPool.ts | 11 +- src/connectors/uniswap/schemas.ts | 110 ------------------ .../trading-amm-routes/add-liquidity.ts | 10 +- src/trading/trading-amm-routes/create-pool.ts | 101 +++++++--------- .../trading-amm-routes/execute-swap.ts | 10 +- .../trading-amm-routes/quote-liquidity.ts | 10 +- src/trading/trading-amm-routes/quote-swap.ts | 10 +- .../trading-amm-routes/remove-liquidity.ts | 10 +- 16 files changed, 108 insertions(+), 399 deletions(-) diff --git a/src/connectors/pancakeswap/amm-routes/addLiquidity.ts b/src/connectors/pancakeswap/amm-routes/addLiquidity.ts index b992bfef1a..c83fac5b1e 100644 --- a/src/connectors/pancakeswap/amm-routes/addLiquidity.ts +++ b/src/connectors/pancakeswap/amm-routes/addLiquidity.ts @@ -30,8 +30,6 @@ async function addLiquidityInternal( baseTokenAmount: number, quoteTokenAmount: number, slippagePct: number = PancakeswapConfig.config.slippagePct, - gasPrice?: string, - maxGas?: number, ): Promise { const networkToUse = network; @@ -180,8 +178,7 @@ async function addLiquidityInternal( // Add liquidity Token + ETH // Convert gasPrice from wei to gwei if provided - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_ADD_LIQUIDITY_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_ADD_LIQUIDITY_GAS_LIMIT); gasOptions.value = quote.rawQuoteTokenAmount; tx = await router.addLiquidityETH( @@ -242,8 +239,7 @@ async function addLiquidityInternal( // Add liquidity Token + Token // Convert gasPrice from wei to gwei if provided - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_ADD_LIQUIDITY_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_ADD_LIQUIDITY_GAS_LIMIT); tx = await router.addLiquidity( quote.baseTokenObj.address, @@ -291,8 +287,6 @@ export async function addLiquidity( baseTokenAmount: number, quoteTokenAmount: number, slippagePct: number = PancakeswapConfig.config.slippagePct, - gasPrice?: string, - maxGas?: number, ): Promise { const poolInfo = await getPancakeswapPoolInfo(poolAddress, network, 'amm'); if (!poolInfo) throw httpErrors.notFound(`Pool not found: ${poolAddress}`); @@ -306,8 +300,6 @@ export async function addLiquidity( baseTokenAmount, quoteTokenAmount, slippagePct, - gasPrice, - maxGas, ); } @@ -338,8 +330,6 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { quoteTokenAmount, slippagePct, walletAddress: requestedWalletAddress, - gasPrice, - maxGas, } = request.body; // Validate essential parameters @@ -366,8 +356,6 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { baseTokenAmount, quoteTokenAmount, slippagePct, - gasPrice, - maxGas, ); } catch (e) { logger.error(e); diff --git a/src/connectors/pancakeswap/amm-routes/createPool.ts b/src/connectors/pancakeswap/amm-routes/createPool.ts index 148caefae9..4d0bb43df7 100644 --- a/src/connectors/pancakeswap/amm-routes/createPool.ts +++ b/src/connectors/pancakeswap/amm-routes/createPool.ts @@ -80,8 +80,6 @@ export async function createPool( baseTokenAmount: number, quoteTokenAmount?: number, initialPrice?: number, - gasPrice?: number, - maxGas?: number, slippagePct: number = PancakeswapConfig.config.slippagePct, ): Promise { if (baseTokenAmount <= 0) { @@ -178,10 +176,6 @@ export async function createPool( const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now - // gasPrice arrives already in gwei (the unit prepareGasOptions expects). The connector's Fastify - // route accepts gasPrice as a wei string (sibling shape) and converts it to gwei before calling. - const gasPriceGwei = gasPrice; - let tx; if (baseIsEth || quoteIsEth) { // One side is ETH/WETH → addLiquidityETH. The ERC20 side needs an allowance to the router; the @@ -204,7 +198,7 @@ export async function createPool( ); } - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_CREATE_POOL_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_CREATE_POOL_GAS_LIMIT); gasOptions.value = ethRawAmount; tx = await router.addLiquidityETH( @@ -248,7 +242,7 @@ export async function createPool( ); } - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_CREATE_POOL_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_CREATE_POOL_GAS_LIMIT); tx = await router.addLiquidity( baseTokenInfo.address, @@ -322,8 +316,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { quoteTokenAmount, initialPrice, slippagePct, - gasPrice, - maxGas, walletAddress: requestedWalletAddress, } = request.body; @@ -340,9 +332,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { logger.info(`Using first available wallet address: ${walletAddress}`); } - // Route accepts gasPrice as a wei string (matching sibling AMM requests); createPool expects gwei. - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - return await createPool( network, walletAddress, @@ -351,8 +340,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { baseTokenAmount, quoteTokenAmount, initialPrice, - gasPriceGwei, - maxGas, slippagePct, ); } catch (e) { diff --git a/src/connectors/pancakeswap/amm-routes/removeLiquidity.ts b/src/connectors/pancakeswap/amm-routes/removeLiquidity.ts index f50da1bfd2..6acbd8fc6a 100644 --- a/src/connectors/pancakeswap/amm-routes/removeLiquidity.ts +++ b/src/connectors/pancakeswap/amm-routes/removeLiquidity.ts @@ -33,8 +33,6 @@ export async function removeLiquidity( poolAddress: string, percentageToRemove: number, slippagePct: number = PancakeswapConfig.config.slippagePct, - gasPrice?: string, - maxGas?: number, ): Promise { if (!poolAddress || !percentageToRemove) throw httpErrors.badRequest('Missing required parameters'); if (percentageToRemove <= 0 || percentageToRemove > 100) { @@ -89,8 +87,7 @@ export async function removeLiquidity( await checkLPAllowance(ethereum, wallet, poolAddress, routerAddress, liquidityToRemove); const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_REMOVE_LIQUIDITY_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_REMOVE_LIQUIDITY_GAS_LIMIT); let tx; if (baseTokenObj.symbol === 'WETH') { @@ -162,14 +159,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { - network, - poolAddress, - percentageToRemove, - walletAddress: requestedWalletAddress, - gasPrice, - maxGas, - } = request.body; + const { network, poolAddress, percentageToRemove, walletAddress: requestedWalletAddress } = request.body; let walletAddress = requestedWalletAddress; if (!walletAddress) { @@ -180,15 +170,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { logger.info(`Using first available wallet address: ${walletAddress}`); } - return await removeLiquidity( - network, - walletAddress, - poolAddress, - percentageToRemove, - undefined, - gasPrice, - maxGas, - ); + return await removeLiquidity(network, walletAddress, poolAddress, percentageToRemove, undefined); } catch (e) { logger.error(e); if (e.statusCode) throw e; diff --git a/src/connectors/pancakeswap/clmm-routes/createPool.ts b/src/connectors/pancakeswap/clmm-routes/createPool.ts index a135888c0c..78f43b8d8a 100644 --- a/src/connectors/pancakeswap/clmm-routes/createPool.ts +++ b/src/connectors/pancakeswap/clmm-routes/createPool.ts @@ -78,8 +78,6 @@ export async function createPool( quoteToken: string, initialPrice?: number, fee?: number, - gasPrice?: number, - maxGas?: number, ): Promise { // Validate the fee tier — V3 only accepts a fixed set of tiers, each mapped to a tick spacing. if (fee === undefined) { @@ -174,7 +172,7 @@ export async function createPool( const nftManagerAddress = getPancakeswapV3NftManagerAddress(network); const nftManager = new Contract(nftManagerAddress, INftManagerCreatePoolABI, wallet); - const gasOptions = await ethereum.prepareGasOptions(gasPrice, maxGas || CLMM_CREATE_POOL_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, CLMM_CREATE_POOL_GAS_LIMIT); const tx = await nftManager.createAndInitializePoolIfNecessary( token0.address, @@ -239,8 +237,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { quoteToken, fee, initialPrice, - gasPrice, - maxGas, walletAddress: requestedWalletAddress, } = request.body; @@ -257,10 +253,7 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { logger.info(`Using first available wallet address: ${walletAddress}`); } - // Route accepts gasPrice as a wei string (matching sibling requests); createPool expects gwei. - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - - return await createPool(network, walletAddress, baseToken, quoteToken, initialPrice, fee, gasPriceGwei, maxGas); + return await createPool(network, walletAddress, baseToken, quoteToken, initialPrice, fee); } catch (e) { logger.error(e); if (e.statusCode) { diff --git a/src/connectors/pancakeswap/schemas.ts b/src/connectors/pancakeswap/schemas.ts index ba5b9ec143..62776deb00 100644 --- a/src/connectors/pancakeswap/schemas.ts +++ b/src/connectors/pancakeswap/schemas.ts @@ -80,17 +80,6 @@ export const PancakeswapAmmCreatePoolRequest = Type.Object({ default: PancakeswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // ======================================== @@ -160,17 +149,6 @@ export const PancakeswapClmmCreatePoolRequest = Type.Object({ 'unified swap router so the pool opens on-market and is not immediately arbitraged.', }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [600000], - }), - ), }); // ======================================== @@ -309,17 +287,6 @@ export const PancakeswapAmmAddLiquidityRequest = Type.Object({ default: PancakeswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Pancakeswap AMM Remove Liquidity Request @@ -345,17 +312,6 @@ export const PancakeswapAmmRemoveLiquidityRequest = Type.Object({ maximum: 100, description: 'Percentage of liquidity to remove', }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Pancakeswap AMM Execute Swap Request @@ -494,17 +450,6 @@ export const PancakeswapClmmOpenPositionRequest = Type.Object({ default: PancakeswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Pancakeswap CLMM Add Liquidity Request @@ -540,17 +485,6 @@ export const PancakeswapClmmAddLiquidityRequest = Type.Object({ default: PancakeswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Pancakeswap CLMM Remove Liquidity Request @@ -577,17 +511,6 @@ export const PancakeswapClmmRemoveLiquidityRequest = Type.Object({ maximum: 100, description: 'Percentage of liquidity to remove', }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Pancakeswap CLMM Close Position Request @@ -609,17 +532,6 @@ export const PancakeswapClmmClosePositionRequest = Type.Object({ positionAddress: Type.String({ description: 'NFT token ID of the position to close', }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Pancakeswap CLMM Collect Fees Request @@ -641,17 +553,6 @@ export const PancakeswapClmmCollectFeesRequest = Type.Object({ positionAddress: Type.String({ description: 'NFT token ID of the position', }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Pancakeswap CLMM Execute Swap Request @@ -701,15 +602,4 @@ export const PancakeswapClmmExecuteSwapRequest = Type.Object({ default: PancakeswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); diff --git a/src/connectors/uniswap/amm-routes/addLiquidity.ts b/src/connectors/uniswap/amm-routes/addLiquidity.ts index 62b46cdfa8..f8dec168aa 100644 --- a/src/connectors/uniswap/amm-routes/addLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/addLiquidity.ts @@ -31,8 +31,6 @@ async function addLiquidityInternal( baseTokenAmount: number, quoteTokenAmount: number, slippagePct: number = UniswapConfig.config.slippagePct, - gasPrice?: string, - maxGas?: number, ): Promise { const networkToUse = network; @@ -182,8 +180,7 @@ async function addLiquidityInternal( // Add liquidity Token + ETH // Convert gasPrice from wei to gwei if provided - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_ADD_LIQUIDITY_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_ADD_LIQUIDITY_GAS_LIMIT); gasOptions.value = quote.rawQuoteTokenAmount; tx = await router.addLiquidityETH( @@ -244,8 +241,7 @@ async function addLiquidityInternal( // Add liquidity Token + Token // Convert gasPrice from wei to gwei if provided - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_ADD_LIQUIDITY_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_ADD_LIQUIDITY_GAS_LIMIT); tx = await router.addLiquidity( quote.baseTokenObj.address, @@ -293,8 +289,6 @@ export async function addLiquidity( baseTokenAmount: number, quoteTokenAmount: number, slippagePct: number = UniswapConfig.config.slippagePct, - gasPrice?: string, - maxGas?: number, ): Promise { const poolInfo = await getUniswapPoolInfo(poolAddress, network, 'amm'); if (!poolInfo) throw httpErrors.notFound(`Pool not found: ${poolAddress}`); @@ -308,8 +302,6 @@ export async function addLiquidity( baseTokenAmount, quoteTokenAmount, slippagePct, - gasPrice, - maxGas, ); } @@ -341,8 +333,6 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { quoteTokenAmount, slippagePct, walletAddress: requestedWalletAddress, - gasPrice, - maxGas, } = request.body; // Validate essential parameters @@ -369,8 +359,6 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { baseTokenAmount, quoteTokenAmount, slippagePct, - gasPrice, - maxGas, ); } catch (e) { logger.error(e); diff --git a/src/connectors/uniswap/amm-routes/createPool.ts b/src/connectors/uniswap/amm-routes/createPool.ts index 945078d9b6..c37b4a5408 100644 --- a/src/connectors/uniswap/amm-routes/createPool.ts +++ b/src/connectors/uniswap/amm-routes/createPool.ts @@ -80,8 +80,6 @@ export async function createPool( baseTokenAmount: number, quoteTokenAmount?: number, initialPrice?: number, - gasPrice?: number, - maxGas?: number, slippagePct: number = UniswapConfig.config.slippagePct, ): Promise { if (baseTokenAmount <= 0) { @@ -178,10 +176,6 @@ export async function createPool( const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now - // gasPrice arrives already in gwei (the unit prepareGasOptions expects). The connector's Fastify - // route accepts gasPrice as a wei string (sibling shape) and converts it to gwei before calling. - const gasPriceGwei = gasPrice; - let tx; if (baseIsEth || quoteIsEth) { // One side is ETH/WETH → addLiquidityETH. The ERC20 side needs an allowance to the router; the @@ -204,7 +198,7 @@ export async function createPool( ); } - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_CREATE_POOL_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_CREATE_POOL_GAS_LIMIT); gasOptions.value = ethRawAmount; tx = await router.addLiquidityETH( @@ -248,7 +242,7 @@ export async function createPool( ); } - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_CREATE_POOL_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_CREATE_POOL_GAS_LIMIT); tx = await router.addLiquidity( baseTokenInfo.address, @@ -322,8 +316,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { quoteTokenAmount, initialPrice, slippagePct, - gasPrice, - maxGas, walletAddress: requestedWalletAddress, } = request.body; @@ -340,9 +332,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { logger.info(`Using first available wallet address: ${walletAddress}`); } - // Route accepts gasPrice as a wei string (matching sibling AMM requests); createPool expects gwei. - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - return await createPool( network, walletAddress, @@ -351,8 +340,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { baseTokenAmount, quoteTokenAmount, initialPrice, - gasPriceGwei, - maxGas, slippagePct, ); } catch (e) { diff --git a/src/connectors/uniswap/amm-routes/removeLiquidity.ts b/src/connectors/uniswap/amm-routes/removeLiquidity.ts index cfe1f9d645..25b0239c1c 100644 --- a/src/connectors/uniswap/amm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/removeLiquidity.ts @@ -29,8 +29,6 @@ export async function removeLiquidity( poolAddress: string, percentageToRemove: number, slippagePct: number = UniswapConfig.config.slippagePct, - gasPrice?: string, - maxGas?: number, ): Promise { if (!poolAddress || !percentageToRemove) throw httpErrors.badRequest('Missing required parameters'); if (percentageToRemove <= 0 || percentageToRemove > 100) { @@ -85,8 +83,7 @@ export async function removeLiquidity( await checkLPAllowance(ethereum, wallet, poolAddress, routerAddress, liquidityToRemove); const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - const gasOptions = await ethereum.prepareGasOptions(gasPriceGwei, maxGas || AMM_REMOVE_LIQUIDITY_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, AMM_REMOVE_LIQUIDITY_GAS_LIMIT); let tx; if (baseTokenObj.symbol === 'WETH') { @@ -159,14 +156,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { - network, - poolAddress, - percentageToRemove, - walletAddress: requestedWalletAddress, - gasPrice, - maxGas, - } = request.body; + const { network, poolAddress, percentageToRemove, walletAddress: requestedWalletAddress } = request.body; let walletAddress = requestedWalletAddress; if (!walletAddress) { @@ -177,15 +167,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { logger.info(`Using first available wallet address: ${walletAddress}`); } - return await removeLiquidity( - network, - walletAddress, - poolAddress, - percentageToRemove, - undefined, - gasPrice, - maxGas, - ); + return await removeLiquidity(network, walletAddress, poolAddress, percentageToRemove, undefined); } catch (e) { logger.error(e); if (e.statusCode) throw e; diff --git a/src/connectors/uniswap/clmm-routes/createPool.ts b/src/connectors/uniswap/clmm-routes/createPool.ts index 1d69b4d0dd..2bc7ae11d7 100644 --- a/src/connectors/uniswap/clmm-routes/createPool.ts +++ b/src/connectors/uniswap/clmm-routes/createPool.ts @@ -77,8 +77,6 @@ export async function createPool( quoteToken: string, initialPrice?: number, fee?: number, - gasPrice?: number, - maxGas?: number, ): Promise { // Validate the fee tier — V3 only accepts a fixed set of tiers, each mapped to a tick spacing. if (fee === undefined) { @@ -176,7 +174,7 @@ export async function createPool( const nftManagerAddress = getUniswapV3NftManagerAddress(network); const nftManager = new Contract(nftManagerAddress, INftManagerCreatePoolABI, wallet); - const gasOptions = await ethereum.prepareGasOptions(gasPrice, maxGas || CLMM_CREATE_POOL_GAS_LIMIT); + const gasOptions = await ethereum.prepareGasOptions(undefined, CLMM_CREATE_POOL_GAS_LIMIT); const tx = await nftManager.createAndInitializePoolIfNecessary( token0.address, @@ -241,8 +239,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { quoteToken, fee, initialPrice, - gasPrice, - maxGas, walletAddress: requestedWalletAddress, } = request.body; @@ -259,10 +255,7 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { logger.info(`Using first available wallet address: ${walletAddress}`); } - // Route accepts gasPrice as a wei string (matching sibling requests); createPool expects gwei. - const gasPriceGwei = gasPrice ? parseFloat(utils.formatUnits(gasPrice, 'gwei')) : undefined; - - return await createPool(network, walletAddress, baseToken, quoteToken, initialPrice, fee, gasPriceGwei, maxGas); + return await createPool(network, walletAddress, baseToken, quoteToken, initialPrice, fee); } catch (e) { logger.error(e); if (e.statusCode) { diff --git a/src/connectors/uniswap/schemas.ts b/src/connectors/uniswap/schemas.ts index e17603a95e..535174c571 100644 --- a/src/connectors/uniswap/schemas.ts +++ b/src/connectors/uniswap/schemas.ts @@ -197,17 +197,6 @@ export const UniswapAmmAddLiquidityRequest = Type.Object({ default: UniswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Uniswap AMM Create Pool Request @@ -258,17 +247,6 @@ export const UniswapAmmCreatePoolRequest = Type.Object({ default: UniswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Uniswap CLMM Create Pool Request (Uniswap V3) @@ -306,17 +284,6 @@ export const UniswapClmmCreatePoolRequest = Type.Object({ 'unified swap router so the pool opens on-market and is not immediately arbitraged.', }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [600000], - }), - ), }); // Uniswap AMM Remove Liquidity Request @@ -342,17 +309,6 @@ export const UniswapAmmRemoveLiquidityRequest = Type.Object({ maximum: 100, description: 'Percentage of liquidity to remove', }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Uniswap AMM Execute Swap Request @@ -490,17 +446,6 @@ export const UniswapClmmOpenPositionRequest = Type.Object({ default: UniswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Uniswap CLMM Add Liquidity Request @@ -535,17 +480,6 @@ export const UniswapClmmAddLiquidityRequest = Type.Object({ default: UniswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Uniswap CLMM Remove Liquidity Request @@ -571,17 +505,6 @@ export const UniswapClmmRemoveLiquidityRequest = Type.Object({ maximum: 100, description: 'Percentage of liquidity to remove', }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Uniswap CLMM Close Position Request @@ -602,17 +525,6 @@ export const UniswapClmmClosePositionRequest = Type.Object({ positionAddress: Type.String({ description: 'NFT token ID of the position to close', }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Uniswap CLMM Collect Fees Request @@ -633,17 +545,6 @@ export const UniswapClmmCollectFeesRequest = Type.Object({ positionAddress: Type.String({ description: 'NFT token ID of the position', }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); // Uniswap CLMM Execute Swap Request @@ -692,15 +593,4 @@ export const UniswapClmmExecuteSwapRequest = Type.Object({ default: UniswapConfig.config.slippagePct, }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); diff --git a/src/trading/trading-amm-routes/add-liquidity.ts b/src/trading/trading-amm-routes/add-liquidity.ts index 6b02ee97d6..f42c2209cf 100644 --- a/src/trading/trading-amm-routes/add-liquidity.ts +++ b/src/trading/trading-amm-routes/add-liquidity.ts @@ -28,7 +28,15 @@ const UnifiedAmmAddLiquidityRequest = Type.Object({ 'position. Ignored by fungible-LP AMMs.', }), ), - slippagePct: Type.Optional(Type.Number({ minimum: 0, maximum: 100 })), + slippagePct: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'Maximum acceptable slippage percentage', + default: 1, + examples: [1], + }), + ), }); export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-amm-routes/create-pool.ts b/src/trading/trading-amm-routes/create-pool.ts index a72e8c4b24..24756f1285 100644 --- a/src/trading/trading-amm-routes/create-pool.ts +++ b/src/trading/trading-amm-routes/create-pool.ts @@ -7,7 +7,7 @@ import { createPool as meteoraCreatePool } from '../../connectors/meteora/amm-ro import { createPool as pancakeswapCreatePool } from '../../connectors/pancakeswap/amm-routes/createPool'; import { createPool as raydiumCreatePool } from '../../connectors/raydium/amm-routes/createPool'; import { createPool as uniswapCreatePool } from '../../connectors/uniswap/amm-routes/createPool'; -import { CreatePoolResponse, CreatePoolResponseType } from '../../schemas/amm-schema'; +import { CreatePoolRequest, CreatePoolResponse, CreatePoolResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; @@ -34,55 +34,50 @@ function parseChainNetwork(chainNetwork: string): { chain: string; network: stri return { chain: parts[0], network: parts.slice(1).join('-') }; } -// Unified schema with a connector field. Per-connector create-pool extras are optional -// and only consumed by their owning connector (configAddress → meteora, feeConfigIndex → -// raydium, gasPrice/maxGas/slippagePct → uniswap). See docs/connectors/meteora-damm-v2.md. -const UnifiedCreatePoolRequest = Type.Object({ - connector: Type.String({ - description: 'AMM connector name (meteora, raydium, uniswap)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), - walletAddress: Type.String({ - description: 'Wallet address (pool creator + payer)', - default: defaultWallet, - }), - baseToken: Type.String({ description: 'Base token symbol or address (becomes the pool base)' }), - quoteToken: Type.String({ description: 'Quote token symbol or address (becomes the pool quote)' }), - baseTokenAmount: Type.Number({ description: 'Amount of base token to seed the pool with' }), - quoteTokenAmount: Type.Optional( - Type.Number({ - description: - 'Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. ' + - 'If omitted (and no initialPrice), the price is fetched from the market.', +// Composed from the canonical CreatePoolRequest (schemas/amm-schema.ts): the +// unified route swaps per-connector `network` for connector + chainNetwork, +// defaults the wallet, and adds the per-protocol fee-config selectors. +const UnifiedCreatePoolRequest = Type.Composite([ + Type.Object({ + connector: Type.String({ + description: 'AMM connector name (meteora, raydium, uniswap)', + default: 'meteora', + examples: ['meteora'], }), - ), - initialPrice: Type.Optional( - Type.Number({ - description: - 'Initial price as quote per base. Overrides quoteTokenAmount. If both are omitted, the current ' + - 'market price is fetched from the unified swap router so the pool opens on-market.', + chainNetwork: Type.String({ + description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', + default: 'solana-mainnet-beta', + examples: ['solana-mainnet-beta'], }), - ), - // Connector-specific create-pool params (optional; ignored by connectors that do not use them): - configAddress: Type.Optional( - Type.String({ description: 'Meteora DAMM v2 config account address (required for the meteora connector)' }), - ), - feeConfigIndex: Type.Optional( - Type.Number({ description: 'Raydium CPMM fee config index (optional; defaults to the first available config)' }), - ), - openTime: Type.Optional(Type.Number({ description: 'Raydium CPMM pool open time (unix seconds; optional)' })), - gasPrice: Type.Optional(Type.Number({ description: 'Uniswap (EVM) gas price in gwei (optional)' })), - maxGas: Type.Optional(Type.Number({ description: 'Uniswap (EVM) max gas limit (optional)' })), - slippagePct: Type.Optional( - Type.Number({ minimum: 0, maximum: 100, description: 'Uniswap seeding slippage percentage (optional)' }), - ), -}); + walletAddress: Type.String({ + description: 'Wallet address (pool creator + payer)', + default: defaultWallet, + }), + configAddress: Type.Optional( + Type.String({ + description: + 'Meteora DAMM v2 config account address (required for the meteora connector — configs are ' + + 'permissionless accounts with no index derivation, so the address must be explicit).', + }), + ), + ammConfigIndex: Type.Optional( + Type.Number({ + description: 'Raydium CPMM fee-config index (optional; defaults to the first available config).', + }), + ), + openTime: Type.Optional(Type.Number({ description: 'Raydium CPMM pool open time (unix seconds; optional)' })), + slippagePct: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'Uniswap seeding slippage percentage', + default: 1, + examples: [1], + }), + ), + }), + Type.Omit(CreatePoolRequest, ['network', 'walletAddress'], {}), +]); export const createPoolRoute: FastifyPluginAsync = async (fastify) => { fastify.post<{ @@ -113,10 +108,8 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { quoteTokenAmount, initialPrice, configAddress, - feeConfigIndex, + ammConfigIndex, openTime, - gasPrice, - maxGas, slippagePct, } = request.body; @@ -144,7 +137,7 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { baseTokenAmount, quoteTokenAmount, initialPrice, - feeConfigIndex, + ammConfigIndex, openTime, ); @@ -157,8 +150,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { baseTokenAmount, quoteTokenAmount, initialPrice, - gasPrice, - maxGas, slippagePct, ); @@ -171,8 +162,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { baseTokenAmount, quoteTokenAmount, initialPrice, - gasPrice, - maxGas, slippagePct, ); diff --git a/src/trading/trading-amm-routes/execute-swap.ts b/src/trading/trading-amm-routes/execute-swap.ts index bbdceb1912..ac10ffae7d 100644 --- a/src/trading/trading-amm-routes/execute-swap.ts +++ b/src/trading/trading-amm-routes/execute-swap.ts @@ -22,7 +22,15 @@ const UnifiedAmmExecuteSwapRequest = Type.Object({ baseToken: Type.String({ description: 'Base token symbol or address (determines swap direction)' }), amount: Type.Number({ description: 'Amount denominated in the base token' }), side: Type.String({ description: 'Trade direction', enum: ['BUY', 'SELL'] }), - slippagePct: Type.Optional(Type.Number({ minimum: 0, maximum: 100 })), + slippagePct: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'Maximum acceptable slippage percentage', + default: 1, + examples: [1], + }), + ), }); export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-amm-routes/quote-liquidity.ts b/src/trading/trading-amm-routes/quote-liquidity.ts index 49d073ad28..1d1989640c 100644 --- a/src/trading/trading-amm-routes/quote-liquidity.ts +++ b/src/trading/trading-amm-routes/quote-liquidity.ts @@ -20,7 +20,15 @@ const UnifiedAmmQuoteLiquidityRequest = Type.Object({ poolAddress: Type.String({ description: 'Pool contract address' }), baseTokenAmount: Type.Number({ description: 'Amount of base token to deposit' }), quoteTokenAmount: Type.Number({ description: 'Amount of quote token to deposit' }), - slippagePct: Type.Optional(Type.Number({ minimum: 0, maximum: 100 })), + slippagePct: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'Maximum acceptable slippage percentage', + default: 1, + examples: [1], + }), + ), }); export const quoteLiquidityRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-amm-routes/quote-swap.ts b/src/trading/trading-amm-routes/quote-swap.ts index 72e467a6aa..b2ea6f613e 100644 --- a/src/trading/trading-amm-routes/quote-swap.ts +++ b/src/trading/trading-amm-routes/quote-swap.ts @@ -21,7 +21,15 @@ const UnifiedAmmQuoteSwapRequest = Type.Object({ baseToken: Type.String({ description: 'Base token symbol or address (determines swap direction)' }), amount: Type.Number({ description: 'Amount denominated in the base token' }), side: Type.String({ description: 'Trade direction', enum: ['BUY', 'SELL'] }), - slippagePct: Type.Optional(Type.Number({ minimum: 0, maximum: 100 })), + slippagePct: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'Maximum acceptable slippage percentage', + default: 1, + examples: [1], + }), + ), }); export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-amm-routes/remove-liquidity.ts b/src/trading/trading-amm-routes/remove-liquidity.ts index f1c9aff3d5..eaee68647d 100644 --- a/src/trading/trading-amm-routes/remove-liquidity.ts +++ b/src/trading/trading-amm-routes/remove-liquidity.ts @@ -27,7 +27,15 @@ const UnifiedAmmRemoveLiquidityRequest = Type.Object({ }), ), percentageToRemove: Type.Number({ minimum: 0, maximum: 100, description: 'Percentage of liquidity to remove' }), - slippagePct: Type.Optional(Type.Number({ minimum: 0, maximum: 100 })), + slippagePct: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'Maximum acceptable slippage percentage', + default: 1, + examples: [1], + }), + ), }); export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { From 7a36c6abf7a8bd3c2a5563ec3e2a95380eafb15b Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 17:20:14 -0700 Subject: [PATCH 16/31] =?UTF-8?q?refactor(swap):=20uniform=20router=20cont?= =?UTF-8?q?ract=20=E2=80=94=20slippagePct=20+=20approximateIfNoExactOut=20?= =?UTF-8?q?only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- .../0x/router-routes/executeQuote.ts | 16 ++--- .../0x/router-routes/executeSwap.ts | 8 +-- src/connectors/0x/schemas.ts | 22 ------ src/connectors/dflow/schemas.ts | 4 +- .../jupiter/router-routes/executeQuote.ts | 14 +--- .../jupiter/router-routes/executeSwap.ts | 30 ++------ .../jupiter/router-routes/quoteSwap.ts | 27 ++----- src/connectors/jupiter/schemas.ts | 54 +------------- src/connectors/okx/schemas.ts | 4 +- src/connectors/titan/schemas.ts | 4 +- src/trading/swap/execute.ts | 71 ++++++++++++++++--- src/trading/swap/quote.ts | 56 +++++++++++++-- test/connectors/jupiter/schemas.test.ts | 25 ++++--- 13 files changed, 154 insertions(+), 181 deletions(-) diff --git a/src/connectors/0x/router-routes/executeQuote.ts b/src/connectors/0x/router-routes/executeQuote.ts index f49f185496..d1d2ac72b3 100644 --- a/src/connectors/0x/router-routes/executeQuote.ts +++ b/src/connectors/0x/router-routes/executeQuote.ts @@ -9,13 +9,7 @@ import { quoteCache } from '../../../services/quote-cache'; import { ZeroX } from '../0x'; import { ZeroXExecuteQuoteRequest } from '../schemas'; -async function executeQuote( - walletAddress: string, - network: string, - quoteId: string, - gasPrice?: string, - maxGas?: number, -): Promise { +async function executeQuote(walletAddress: string, network: string, quoteId: string): Promise { // Retrieve cached quote from global cache const quote = quoteCache.get(quoteId); if (!quote) { @@ -56,8 +50,7 @@ async function executeQuote( to: quote.to, data: quote.data, value: quote.value, - gasLimit: maxGas || parseInt(quote.estimatedGas || quote.gas), - ...(gasPrice && { gasPrice: BigNumber.from(gasPrice) }), + gasLimit: parseInt(quote.estimatedGas || quote.gas), }; const txResponse = await wallet.sendTransaction(txData); @@ -125,10 +118,9 @@ export const executeQuoteRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { walletAddress, network, quoteId, gasPrice, maxGas } = - request.body as typeof ZeroXExecuteQuoteRequest._type; + const { walletAddress, network, quoteId } = request.body as typeof ZeroXExecuteQuoteRequest._type; - return await executeQuote(walletAddress, network, quoteId, gasPrice, maxGas); + return await executeQuote(walletAddress, network, quoteId); } catch (e) { if (e.statusCode) throw e; logger.error('Error executing 0x quote:', e); diff --git a/src/connectors/0x/router-routes/executeSwap.ts b/src/connectors/0x/router-routes/executeSwap.ts index 8e9aa34beb..c899b9df41 100644 --- a/src/connectors/0x/router-routes/executeSwap.ts +++ b/src/connectors/0x/router-routes/executeSwap.ts @@ -17,8 +17,6 @@ async function executeSwap( amount: number, side: 'BUY' | 'SELL', slippagePct: number = ZeroXConfig.config.slippagePct, - gasPrice?: string, - maxGas?: number, ): Promise { // Step 1: Get a fresh firm quote using the quoteSwap function const quoteResult = await quoteSwap( @@ -33,7 +31,7 @@ async function executeSwap( ); // Step 2: Execute the quote immediately using executeQuote function - const executeResult = await executeQuote(walletAddress, network, quoteResult.quoteId, gasPrice, maxGas); + const executeResult = await executeQuote(walletAddress, network, quoteResult.quoteId); return executeResult; } @@ -56,7 +54,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { walletAddress, network, baseToken, quoteToken, amount, side, slippagePct, gasPrice, maxGas } = + const { walletAddress, network, baseToken, quoteToken, amount, side, slippagePct } = request.body as typeof ZeroXExecuteSwapRequest._type; return await executeSwap( @@ -67,8 +65,6 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { amount, side as 'BUY' | 'SELL', slippagePct, - gasPrice, - maxGas, ); } catch (e) { if (e.statusCode) throw e; diff --git a/src/connectors/0x/schemas.ts b/src/connectors/0x/schemas.ts index dbd0a0301f..cae5beaf87 100644 --- a/src/connectors/0x/schemas.ts +++ b/src/connectors/0x/schemas.ts @@ -145,17 +145,6 @@ export const ZeroXExecuteQuoteRequest = Type.Object({ description: 'ID of the quote to execute', examples: ['123e4567-e89b-12d3-a456-426614174000'], }), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [1000000], - }), - ), }); // 0x-specific execute-swap request (superset of base ExecuteSwapRequest) @@ -200,15 +189,4 @@ export const ZeroXExecuteSwapRequest = Type.Object({ examples: [1], }), ), - gasPrice: Type.Optional( - Type.String({ - description: 'Gas price in wei for the transaction', - }), - ), - maxGas: Type.Optional( - Type.Number({ - description: 'Maximum gas limit for the transaction', - examples: [300000], - }), - ), }); diff --git a/src/connectors/dflow/schemas.ts b/src/connectors/dflow/schemas.ts index 5398472c47..dfc71e9feb 100644 --- a/src/connectors/dflow/schemas.ts +++ b/src/connectors/dflow/schemas.ts @@ -50,7 +50,7 @@ export const DFlowQuoteSwapRequest = Type.Object({ approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: - 'For BUY orders: DFlow is ExactIn-only, so BUYs are approximated via a sell-leg ExactIn quote. If false, BUY requests fail with a clear error.', + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', default: true, }), ), @@ -161,7 +161,7 @@ export const DFlowExecuteSwapRequest = Type.Object({ approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: - 'For BUY orders: DFlow is ExactIn-only, so BUYs are approximated via a sell-leg ExactIn quote. If false, BUY requests fail with a clear error.', + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', default: true, }), ), diff --git a/src/connectors/jupiter/router-routes/executeQuote.ts b/src/connectors/jupiter/router-routes/executeQuote.ts index 6c289efcd8..5dd3b2d34d 100644 --- a/src/connectors/jupiter/router-routes/executeQuote.ts +++ b/src/connectors/jupiter/router-routes/executeQuote.ts @@ -12,8 +12,6 @@ export async function executeQuote( walletAddress: string, network: string, quoteId: string, - priorityLevel?: string, - maxLamports?: number, ): Promise { // Retrieve cached quote const quote = quoteCache.get(quoteId); @@ -38,12 +36,7 @@ export async function executeQuote( logger.info( `Executing quote ${quoteId} for ${inputToken.symbol} -> ${outputToken.symbol}, slippageBps=${quote.slippageBps}`, ); - const transaction = await jupiter.buildSwapTransactionForHardwareWallet( - walletAddress, - quote, - maxLamports, - priorityLevel, - ); + const transaction = await jupiter.buildSwapTransactionForHardwareWallet(walletAddress, quote); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); const txData = await solana.connection.getTransaction(signature, { @@ -88,10 +81,9 @@ export const executeQuoteRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { walletAddress, network, quoteId, priorityLevel, maxLamports } = - request.body as typeof JupiterExecuteQuoteRequest._type; + const { walletAddress, network, quoteId } = request.body as typeof JupiterExecuteQuoteRequest._type; - return await executeQuote(walletAddress, network, quoteId, priorityLevel, maxLamports); + return await executeQuote(walletAddress, network, quoteId); } catch (e) { if (e.statusCode) throw e; logger.error('Error executing quote:', e); diff --git a/src/connectors/jupiter/router-routes/executeSwap.ts b/src/connectors/jupiter/router-routes/executeSwap.ts index 078170b326..df7abbef47 100644 --- a/src/connectors/jupiter/router-routes/executeSwap.ts +++ b/src/connectors/jupiter/router-routes/executeSwap.ts @@ -17,8 +17,6 @@ async function executeSwap( amount: number, side: 'BUY' | 'SELL', slippagePct: number = JupiterConfig.config.slippagePct, - priorityLevel?: string, - maxLamports?: number, approximateIfNoExactOut: boolean = true, ): Promise { // Step 1: Get a fresh quote using the quoteSwap function @@ -29,19 +27,11 @@ async function executeSwap( amount, side, slippagePct, - undefined, - undefined, approximateIfNoExactOut, ); - // Step 2: Execute the quote immediately using executeQuote function - const executeResult = await executeQuote( - walletAddress, - network, - quoteResult.quoteId, - priorityLevel ?? JupiterConfig.config.priorityLevel, - maxLamports ?? JupiterConfig.config.maxLamports, - ); + // Step 2: Execute the quote immediately (priority fees come from the connector config) + const executeResult = await executeQuote(walletAddress, network, quoteResult.quoteId); return executeResult; } @@ -64,18 +54,8 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { - walletAddress, - network, - baseToken, - quoteToken, - amount, - side, - slippagePct, - priorityLevel, - maxLamports, - approximateIfNoExactOut, - } = request.body as typeof JupiterExecuteSwapRequest._type; + const { walletAddress, network, baseToken, quoteToken, amount, side, slippagePct, approximateIfNoExactOut } = + request.body as typeof JupiterExecuteSwapRequest._type; return await executeSwap( walletAddress, @@ -85,8 +65,6 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { amount, side as 'BUY' | 'SELL', slippagePct, - priorityLevel, - maxLamports, approximateIfNoExactOut, ); } catch (e) { diff --git a/src/connectors/jupiter/router-routes/quoteSwap.ts b/src/connectors/jupiter/router-routes/quoteSwap.ts index a66809bf1e..9fc10f8178 100644 --- a/src/connectors/jupiter/router-routes/quoteSwap.ts +++ b/src/connectors/jupiter/router-routes/quoteSwap.ts @@ -20,8 +20,6 @@ export async function quoteSwap( amount: number, side: 'BUY' | 'SELL', slippagePct: number = JupiterConfig.config.slippagePct, - onlyDirectRoutes?: boolean, - restrictIntermediateTokens?: boolean, approximateIfNoExactOut: boolean = true, ): Promise> { const solana = await Solana.getInstance(network); @@ -43,9 +41,10 @@ export async function quoteSwap( logger.info(`Getting quote for ${amount} ${inputToken.symbol} -> ${outputToken.symbol}`); - const effectiveOnlyDirectRoutes = onlyDirectRoutes ?? JupiterConfig.config.onlyDirectRoutes; - const effectiveRestrictIntermediateTokens = - restrictIntermediateTokens ?? JupiterConfig.config.restrictIntermediateTokens; + // Routing policy comes from the connector config (conf/connectors/jupiter.yml), + // not per-request parameters. + const effectiveOnlyDirectRoutes = JupiterConfig.config.onlyDirectRoutes; + const effectiveRestrictIntermediateTokens = JupiterConfig.config.restrictIntermediateTokens; let quoteResponse; let approximation = false; @@ -105,8 +104,7 @@ export async function quoteSwap( } else { // Pass through Jupiter's error with context const tokenPair = `${sanitizeString(baseToken)} -> ${sanitizeString(quoteToken)}`; - const swapMode = side === 'BUY' ? 'ExactOut' : 'ExactIn'; - throw httpErrors.noRouteFound(`No route found for ${tokenPair} (${swapMode}). ${errorMessage}`); + throw httpErrors.noRouteFound(`No route found for ${tokenPair} (ExactIn). ${errorMessage}`); } } @@ -188,17 +186,8 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { - network, - baseToken, - quoteToken, - amount, - side, - slippagePct, - onlyDirectRoutes, - restrictIntermediateTokens, - approximateIfNoExactOut, - } = request.query as typeof JupiterQuoteSwapRequest._type; + const { network, baseToken, quoteToken, amount, side, slippagePct, approximateIfNoExactOut } = + request.query as typeof JupiterQuoteSwapRequest._type; return await quoteSwap( network, @@ -207,8 +196,6 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { amount, side as 'BUY' | 'SELL', slippagePct, - onlyDirectRoutes, - restrictIntermediateTokens, approximateIfNoExactOut, ); } catch (e) { diff --git a/src/connectors/jupiter/schemas.ts b/src/connectors/jupiter/schemas.ts index e454b2ece5..b486227ff4 100644 --- a/src/connectors/jupiter/schemas.ts +++ b/src/connectors/jupiter/schemas.ts @@ -47,22 +47,10 @@ export const JupiterQuoteSwapRequest = Type.Object({ default: JupiterConfig.config.slippagePct, }), ), - restrictIntermediateTokens: Type.Optional( - Type.Boolean({ - description: 'Restrict routing through highly liquid intermediate tokens only for better price and stability', - default: JupiterConfig.config.restrictIntermediateTokens, - }), - ), - onlyDirectRoutes: Type.Optional( - Type.Boolean({ - description: 'Restrict routing to only go through 1 market', - default: JupiterConfig.config.onlyDirectRoutes, - }), - ), approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: - 'For BUY orders when the pair has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', default: true, }), ), @@ -167,19 +155,6 @@ export const JupiterExecuteQuoteRequest = Type.Object({ description: 'ID of the Jupiter quote to execute', examples: ['123e4567-e89b-12d3-a456-426614174000'], }), - priorityLevel: Type.Optional( - Type.String({ - description: 'Priority level for Solana transaction processing', - enum: ['medium', 'high', 'veryHigh'], - default: JupiterConfig.config.priorityLevel, - }), - ), - maxLamports: Type.Optional( - Type.Number({ - description: 'Maximum priority fee in lamports for Solana transaction', - default: [JupiterConfig.config.maxLamports], - }), - ), }); // Jupiter-specific execute-swap request (superset of base ExecuteSwapRequest) @@ -223,36 +198,11 @@ export const JupiterExecuteSwapRequest = Type.Object({ default: JupiterConfig.config.slippagePct, }), ), - restrictIntermediateTokens: Type.Optional( - Type.Boolean({ - description: 'Restrict routing through highly liquid intermediate tokens only for better price and stability', - default: JupiterConfig.config.restrictIntermediateTokens, - }), - ), - onlyDirectRoutes: Type.Optional( - Type.Boolean({ - description: 'Restrict routing to only go through 1 market', - default: JupiterConfig.config.onlyDirectRoutes, - }), - ), approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: - 'For BUY orders when the pair has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', default: true, }), ), - priorityLevel: Type.Optional( - Type.String({ - description: 'Priority level for Solana transaction processing', - enum: ['medium', 'high', 'veryHigh'], - default: JupiterConfig.config.priorityLevel, - }), - ), - maxLamports: Type.Optional( - Type.Number({ - description: 'Maximum priority fee in lamports for Solana transaction', - default: JupiterConfig.config.maxLamports, - }), - ), }); diff --git a/src/connectors/okx/schemas.ts b/src/connectors/okx/schemas.ts index 7347033f39..cbb70a5fb5 100644 --- a/src/connectors/okx/schemas.ts +++ b/src/connectors/okx/schemas.ts @@ -50,7 +50,7 @@ export const OkxQuoteSwapRequest = Type.Object({ approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: - 'For BUY orders when OKX cannot serve an exactOut quote: approximate via a sell-leg exactIn quote instead of failing', + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', default: true, }), ), @@ -161,7 +161,7 @@ export const OkxExecuteSwapRequest = Type.Object({ approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: - 'For BUY orders when OKX cannot serve an exactOut quote: approximate via a sell-leg exactIn quote instead of failing', + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', default: true, }), ), diff --git a/src/connectors/titan/schemas.ts b/src/connectors/titan/schemas.ts index abbf8a2a19..6256e49a7f 100644 --- a/src/connectors/titan/schemas.ts +++ b/src/connectors/titan/schemas.ts @@ -52,7 +52,7 @@ export const TitanQuoteSwapRequest = Type.Object({ approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: - 'For BUY orders: Titan DART is ExactIn-only, so BUYs are approximated via a sell-leg ExactIn quote. If false, BUY requests fail with a clear error.', + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', default: true, }), ), @@ -170,7 +170,7 @@ export const TitanExecuteSwapRequest = Type.Object({ approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: - 'For BUY orders: Titan DART is ExactIn-only, so BUYs are approximated via a sell-leg ExactIn quote. If false, BUY requests fail with a clear error.', + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing', default: true, }), ), diff --git a/src/trading/swap/execute.ts b/src/trading/swap/execute.ts index ea46dbe360..64d5ec5428 100644 --- a/src/trading/swap/execute.ts +++ b/src/trading/swap/execute.ts @@ -83,6 +83,13 @@ const UnifiedExecuteSwapRequestSchema = Type.Object({ default: 1, }), ), + approximateIfNoExactOut: Type.Optional( + Type.Boolean({ + description: + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing. Solana routers only.', + default: true, + }), + ), }); type UnifiedExecuteSwapRequest = Static; @@ -123,6 +130,7 @@ async function executeSolanaSwap( side: 'BUY' | 'SELL', slippagePct?: number, connector?: string, + approximateIfNoExactOut?: boolean, ): Promise { try { const networkConfig = getSolanaNetworkConfig(network); @@ -163,15 +171,41 @@ async function executeSolanaSwap( amount, side, slippagePct, - undefined, // priorityLevel - undefined, // maxLamports + approximateIfNoExactOut, ); } else if (providerKey === 'dflow/router') { - return await dflowRouterExecuteSwap(walletAddress, network, baseToken, quoteToken, amount, side, slippagePct); + return await dflowRouterExecuteSwap( + walletAddress, + network, + baseToken, + quoteToken, + amount, + side, + slippagePct, + approximateIfNoExactOut, + ); } else if (providerKey === 'okx/router') { - return await okxRouterExecuteSwap(walletAddress, network, baseToken, quoteToken, amount, side, slippagePct); + return await okxRouterExecuteSwap( + walletAddress, + network, + baseToken, + quoteToken, + amount, + side, + slippagePct, + approximateIfNoExactOut, + ); } else if (providerKey === 'titan/router') { - return await titanRouterExecuteSwap(walletAddress, network, baseToken, quoteToken, amount, side, slippagePct); + return await titanRouterExecuteSwap( + walletAddress, + network, + baseToken, + quoteToken, + amount, + side, + slippagePct, + approximateIfNoExactOut, + ); } else if (providerKey === 'raydium/amm') { return await raydiumAmmExecuteSwap(network, walletAddress, poolAddress!, baseToken, side, amount, slippagePct); } else if (providerKey === 'raydium/clmm') { @@ -307,6 +341,7 @@ export async function executeUnifiedSwap( side: 'BUY' | 'SELL', slippagePct?: number, connector?: string, + approximateIfNoExactOut?: boolean, ): Promise { const { chain, network } = parseChainNetwork(chainNetwork); @@ -319,7 +354,17 @@ export async function executeUnifiedSwap( return executeEthereumSwap(network, walletAddress, baseToken, quoteToken, amount, side, slippagePct, connector); case 'solana': - return executeSolanaSwap(network, walletAddress, baseToken, quoteToken, amount, side, slippagePct, connector); + return executeSolanaSwap( + network, + walletAddress, + baseToken, + quoteToken, + amount, + side, + slippagePct, + connector, + approximateIfNoExactOut, + ); default: throw httpErrors.badRequest(`Unsupported chain: ${chain}`); @@ -344,8 +389,17 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { }, }, async (request, reply) => { - const { chainNetwork, walletAddress, baseToken, quoteToken, amount, side, slippagePct, connector } = - request.body as UnifiedExecuteSwapRequest; + const { + chainNetwork, + walletAddress, + baseToken, + quoteToken, + amount, + side, + slippagePct, + approximateIfNoExactOut, + connector, + } = request.body as UnifiedExecuteSwapRequest; try { const result = await executeUnifiedSwap( @@ -357,6 +411,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { side as 'BUY' | 'SELL', slippagePct, connector, + approximateIfNoExactOut, ); return reply.code(200).send(result); } catch (error: any) { diff --git a/src/trading/swap/quote.ts b/src/trading/swap/quote.ts index db4953c5a5..565ebe3e7d 100644 --- a/src/trading/swap/quote.ts +++ b/src/trading/swap/quote.ts @@ -69,6 +69,13 @@ const UnifiedQuoteSwapRequestSchema = Type.Object({ default: 1, }), ), + approximateIfNoExactOut: Type.Optional( + Type.Boolean({ + description: + 'For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing. Solana routers only.', + default: true, + }), + ), }); type UnifiedQuoteSwapRequest = Static; @@ -108,6 +115,7 @@ async function getSolanaQuoteSwap( side: 'BUY' | 'SELL', slippagePct?: number, connector?: string, + approximateIfNoExactOut?: boolean, ): Promise { try { const networkConfig = getSolanaNetworkConfig(network); @@ -147,15 +155,38 @@ async function getSolanaQuoteSwap( amount, side, slippagePct, - undefined, // onlyDirectRoutes - undefined, // restrictIntermediateTokens + approximateIfNoExactOut, ); } else if (providerKey === 'dflow/router') { - return await dflowRouterQuoteSwap(network, baseToken, quoteToken, amount, side, slippagePct); + return await dflowRouterQuoteSwap( + network, + baseToken, + quoteToken, + amount, + side, + slippagePct, + approximateIfNoExactOut, + ); } else if (providerKey === 'okx/router') { - return await okxRouterQuoteSwap(network, baseToken, quoteToken, amount, side, slippagePct); + return await okxRouterQuoteSwap( + network, + baseToken, + quoteToken, + amount, + side, + slippagePct, + approximateIfNoExactOut, + ); } else if (providerKey === 'titan/router') { - return await titanRouterQuoteSwap(network, baseToken, quoteToken, amount, side, slippagePct); + return await titanRouterQuoteSwap( + network, + baseToken, + quoteToken, + amount, + side, + slippagePct, + approximateIfNoExactOut, + ); } else if (providerKey === 'raydium/amm') { return await raydiumAmmQuoteSwap(network, poolAddress!, baseToken, side, amount, slippagePct); } else if (providerKey === 'raydium/clmm') { @@ -257,6 +288,7 @@ export async function getUnifiedQuoteSwap( side: 'BUY' | 'SELL', slippagePct?: number, connector?: string, + approximateIfNoExactOut?: boolean, ): Promise { const { chain, network } = parseChainNetwork(chainNetwork); @@ -269,7 +301,16 @@ export async function getUnifiedQuoteSwap( return getEthereumQuoteSwap(network, baseToken, quoteToken, amount, side, slippagePct, connector); case 'solana': - return getSolanaQuoteSwap(network, baseToken, quoteToken, amount, side, slippagePct, connector); + return getSolanaQuoteSwap( + network, + baseToken, + quoteToken, + amount, + side, + slippagePct, + connector, + approximateIfNoExactOut, + ); default: throw httpErrors.badRequest(`Unsupported chain: ${chain}`); @@ -294,7 +335,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { }, }, async (request, reply) => { - const { chainNetwork, baseToken, quoteToken, amount, side, slippagePct, connector } = + const { chainNetwork, baseToken, quoteToken, amount, side, slippagePct, approximateIfNoExactOut, connector } = request.query as UnifiedQuoteSwapRequest; try { @@ -306,6 +347,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { side as 'BUY' | 'SELL', slippagePct, connector, + approximateIfNoExactOut, ); return reply.code(200).send(result); } catch (error: any) { diff --git a/test/connectors/jupiter/schemas.test.ts b/test/connectors/jupiter/schemas.test.ts index 396d9d24e4..b70b060fe4 100644 --- a/test/connectors/jupiter/schemas.test.ts +++ b/test/connectors/jupiter/schemas.test.ts @@ -121,24 +121,27 @@ describe('Jupiter Schema Tests', () => { }); describe('Jupiter-specific Fields', () => { - it('JupiterQuoteSwapRequest should include Jupiter-specific fields', () => { + // Routing policy (restrictIntermediateTokens/onlyDirectRoutes) and priority fees + // (priorityLevel/maxLamports) are connector-config settings, not request params. + // The one request-level knob shared by every Solana router is approximateIfNoExactOut. + it('JupiterQuoteSwapRequest exposes only the standard router params', () => { const props = Object.keys(Jupiter.JupiterQuoteSwapRequest.properties); - expect(props).toContain('restrictIntermediateTokens'); - expect(props).toContain('onlyDirectRoutes'); + expect(props).toContain('approximateIfNoExactOut'); + expect(props).not.toContain('restrictIntermediateTokens'); + expect(props).not.toContain('onlyDirectRoutes'); }); - it('JupiterExecuteQuoteRequest should include Jupiter-specific fields', () => { + it('JupiterExecuteQuoteRequest exposes no priority-fee params', () => { const props = Object.keys(Jupiter.JupiterExecuteQuoteRequest.properties); - expect(props).toContain('priorityLevel'); - expect(props).toContain('maxLamports'); + expect(props).not.toContain('priorityLevel'); + expect(props).not.toContain('maxLamports'); }); - it('JupiterExecuteSwapRequest should include Jupiter-specific fields', () => { + it('JupiterExecuteSwapRequest exposes only the standard router params', () => { const props = Object.keys(Jupiter.JupiterExecuteSwapRequest.properties); - expect(props).toContain('restrictIntermediateTokens'); - expect(props).toContain('onlyDirectRoutes'); - expect(props).toContain('priorityLevel'); - expect(props).toContain('maxLamports'); + expect(props).toContain('approximateIfNoExactOut'); + expect(props).not.toContain('restrictIntermediateTokens'); + expect(props).not.toContain('priorityLevel'); }); it('JupiterQuoteSwapResponse should include Jupiter-specific fields', () => { From a2b9779346d859e77057fc22968b81bdc5f9b557 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 17:23:00 -0700 Subject: [PATCH 17/31] refactor(amm): drop openTime from unified create-pool; optionals trail the schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/trading/trading-amm-routes/create-pool.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/trading/trading-amm-routes/create-pool.ts b/src/trading/trading-amm-routes/create-pool.ts index 24756f1285..bdaba9ada2 100644 --- a/src/trading/trading-amm-routes/create-pool.ts +++ b/src/trading/trading-amm-routes/create-pool.ts @@ -53,6 +53,10 @@ const UnifiedCreatePoolRequest = Type.Composite([ description: 'Wallet address (pool creator + payer)', default: defaultWallet, }), + }), + Type.Omit(CreatePoolRequest, ['network', 'walletAddress'], {}), + // Optional per-protocol fee-config selectors, last so required fields lead the schema: + Type.Object({ configAddress: Type.Optional( Type.String({ description: @@ -65,7 +69,6 @@ const UnifiedCreatePoolRequest = Type.Composite([ description: 'Raydium CPMM fee-config index (optional; defaults to the first available config).', }), ), - openTime: Type.Optional(Type.Number({ description: 'Raydium CPMM pool open time (unix seconds; optional)' })), slippagePct: Type.Optional( Type.Number({ minimum: 0, @@ -76,7 +79,6 @@ const UnifiedCreatePoolRequest = Type.Composite([ }), ), }), - Type.Omit(CreatePoolRequest, ['network', 'walletAddress'], {}), ]); export const createPoolRoute: FastifyPluginAsync = async (fastify) => { @@ -109,7 +111,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { initialPrice, configAddress, ammConfigIndex, - openTime, slippagePct, } = request.body; @@ -138,7 +139,6 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { quoteTokenAmount, initialPrice, ammConfigIndex, - openTime, ); case 'uniswap': From a67c5725cc6b15ab5275bea3f083e5bae9e094d4 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 17:24:53 -0700 Subject: [PATCH 18/31] fix(clmm): expose strategyType on unified add-liquidity, matching open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/trading/trading-clmm-routes/add.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/trading/trading-clmm-routes/add.ts b/src/trading/trading-clmm-routes/add.ts index 7eddf422a0..5218d25174 100644 --- a/src/trading/trading-clmm-routes/add.ts +++ b/src/trading/trading-clmm-routes/add.ts @@ -86,6 +86,15 @@ const UnifiedAddLiquidityRequest = Type.Object({ examples: [1], }), ), + // Meteora-specific parameter (optional, ignored by other connectors). Without it an + // add falls back to the connector-config default shape, which can silently differ + // from the shape the position was opened with. + strategyType: Type.Optional( + Type.Number({ + description: 'Strategy type for Meteora positions (0=Spot, 1=Curve). Only applies to Meteora connector.', + examples: [0], + }), + ), }); // Import connector functions @@ -116,6 +125,7 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { baseTokenAmount, quoteTokenAmount, slippagePct, + strategyType, } = request.body; // Parse chain and network from chainNetwork parameter @@ -168,6 +178,7 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { baseAmount, quoteAmount, slippagePct, + strategyType, ); case 'pancakeswap-sol': From 6b3383c6e4dd6112d56829e54f84cbefb06281b7 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 17:43:32 -0700 Subject: [PATCH 19/31] refactor(trading): standardize unified route surfaces - 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/app.ts | 4 +- src/trading/clmm/pools.ts | 32 +-------- src/trading/clmm/positions-owned.ts | 52 ++------------- src/trading/clmm/positions.ts | 32 +-------- src/trading/clmm/quote-position.ts | 35 ++-------- src/trading/common.ts | 43 ++++++++++++ src/trading/swap/execute.ts | 45 ++----------- src/trading/swap/quote.ts | 35 ++-------- .../trading-amm-routes/add-liquidity.ts | 10 +-- src/trading/trading-amm-routes/common.ts | 25 ------- src/trading/trading-amm-routes/create-pool.ts | 41 ++---------- .../trading-amm-routes/execute-swap.ts | 10 +-- src/trading/trading-amm-routes/pool-info.ts | 10 +-- .../trading-amm-routes/position-info.ts | 10 +-- .../trading-amm-routes/positions-owned.ts | 10 +-- .../trading-amm-routes/quote-liquidity.ts | 10 +-- src/trading/trading-amm-routes/quote-swap.ts | 10 +-- .../trading-amm-routes/remove-liquidity.ts | 18 ++--- src/trading/trading-clmm-routes/add.ts | 43 +----------- src/trading/trading-clmm-routes/close.ts | 43 +----------- .../trading-clmm-routes/collect-fees.ts | 43 +----------- .../trading-clmm-routes/create-pool.ts | 36 ++-------- src/trading/trading-clmm-routes/open.ts | 48 ++------------ src/trading/trading-clmm-routes/remove.ts | 65 +++++++------------ src/trading/trading.routes.ts | 5 -- test/trading/clmm/routes.test.ts | 3 +- .../trading-amm-routes/create-pool.test.ts | 2 +- .../positions-owned.test.ts | 2 +- .../remove-liquidity.test.ts | 2 +- .../trading-clmm-routes/create-pool.test.ts | 2 +- 30 files changed, 152 insertions(+), 574 deletions(-) create mode 100644 src/trading/common.ts delete mode 100644 src/trading/trading-amm-routes/common.ts diff --git a/src/app.ts b/src/app.ts index 5ef6fe21dd..0eff148624 100644 --- a/src/app.ts +++ b/src/app.ts @@ -45,7 +45,7 @@ import { logger } from './services/logger'; import { quoteCache } from './services/quote-cache'; import { displayChainConfigurations } from './services/startup-banner'; import { tokensRoutes } from './tokens/tokens.routes'; -import { tradingRoutes, tradingClmmRoutes, tradingAmmRoutes } from './trading/trading.routes'; +import { tradingSwapRoutes, tradingClmmRoutes, tradingAmmRoutes } from './trading/trading.routes'; import { GATEWAY_VERSION } from './version'; import { walletRoutes } from './wallet/wallet.routes'; @@ -292,7 +292,7 @@ const configureGatewayServer = () => { app.register(poolRoutes, { prefix: '/pools' }); // Register trading routes (unified cross-chain swap) - app.register(tradingRoutes, { prefix: '/trading/swap' }); + app.register(tradingSwapRoutes, { prefix: '/trading/swap' }); // Register trading CLMM routes (unified cross-chain concentrated liquidity) app.register(tradingClmmRoutes, { prefix: '/trading/clmm' }); diff --git a/src/trading/clmm/pools.ts b/src/trading/clmm/pools.ts index 485890aea7..eb9c5d392f 100644 --- a/src/trading/clmm/pools.ts +++ b/src/trading/clmm/pools.ts @@ -11,6 +11,7 @@ import { getPoolInfo as raydiumGetPoolInfo } from '../../connectors/raydium/clmm import { getPoolInfo as uniswapGetPoolInfo } from '../../connectors/uniswap/clmm-routes/poolInfo'; import { PoolInfo, PoolInfoSchema } from '../../schemas/clmm-schema'; import { logger } from '../../services/logger'; +import { CLMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; // Constants for examples (using Meteora CLMM values) const CLMM_POOL_ADDRESS_EXAMPLE = '2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3'; @@ -19,17 +20,8 @@ const CLMM_POOL_ADDRESS_EXAMPLE = '2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3' * Unified pool info request schema */ const UnifiedPoolInfoRequestSchema = Type.Object({ - connector: Type.String({ - description: 'CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap, orca)', - enum: ['raydium', 'meteora', 'pancakeswap-sol', 'uniswap', 'pancakeswap', 'orca'], - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), poolAddress: Type.String({ description: 'Pool contract address', examples: [CLMM_POOL_ADDRESS_EXAMPLE], @@ -49,24 +41,6 @@ const UnifiedPoolInfoRequestSchema = Type.Object({ type UnifiedPoolInfoRequest = Static; -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} - /** * Get pool info from Solana connectors */ diff --git a/src/trading/clmm/positions-owned.ts b/src/trading/clmm/positions-owned.ts index af6756aa1e..6263c835c3 100644 --- a/src/trading/clmm/positions-owned.ts +++ b/src/trading/clmm/positions-owned.ts @@ -1,8 +1,6 @@ import { Type, Static } from '@sinclair/typebox'; import { FastifyPluginAsync, FastifyInstance } from 'fastify'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; import { getPositionsOwned as meteoraGetPositionsOwned } from '../../connectors/meteora/clmm-routes/positionsOwned'; import { getPositionsOwned as orcaGetPositionsOwned } from '../../connectors/orca/clmm-routes/positionsOwned'; import { getPositionsOwned as pancakeswapGetPositionsOwned } from '../../connectors/pancakeswap/clmm-routes/positionsOwned'; @@ -11,60 +9,22 @@ import { getPositionsOwned as raydiumGetPositionsOwned } from '../../connectors/ import { getPositionsOwned as uniswapGetPositionsOwned } from '../../connectors/uniswap/clmm-routes/positionsOwned'; import { PositionInfo, PositionInfoSchema } from '../../schemas/clmm-schema'; import { logger } from '../../services/logger'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - const solanaChainConfig = getSolanaChainConfig(); - defaultWallet = solanaChainConfig.defaultWallet; -} catch { - const ethereumChainConfig = getEthereumChainConfig(); - defaultWallet = ethereumChainConfig.defaultWallet; -} +import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; /** * Unified positions owned request schema */ const UnifiedPositionsOwnedRequestSchema = Type.Object({ - connector: Type.String({ - description: 'CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap, orca)', - enum: ['raydium', 'meteora', 'pancakeswap-sol', 'uniswap', 'pancakeswap', 'orca'], - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), + walletAddress: Type.String({ + description: 'Wallet address', + default: defaultWallet, }), - walletAddress: Type.Optional( - Type.String({ - description: 'Wallet address (optional, uses default wallet if not provided)', - default: defaultWallet, - }), - ), }); type UnifiedPositionsOwnedRequest = Static; -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} - /** * Get positions owned from Solana connectors */ diff --git a/src/trading/clmm/positions.ts b/src/trading/clmm/positions.ts index 26582eea1d..9f969b6740 100644 --- a/src/trading/clmm/positions.ts +++ b/src/trading/clmm/positions.ts @@ -9,22 +9,14 @@ import { getPositionInfo as raydiumGetPositionInfo } from '../../connectors/rayd import { getPositionInfo as uniswapGetPositionInfo } from '../../connectors/uniswap/clmm-routes/positionInfo'; import { PositionInfo, PositionInfoSchema } from '../../schemas/clmm-schema'; import { logger } from '../../services/logger'; +import { CLMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; /** * Unified position info request schema */ const UnifiedPositionInfoRequestSchema = Type.Object({ - connector: Type.String({ - description: 'CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap, orca)', - enum: ['raydium', 'meteora', 'pancakeswap-sol', 'uniswap', 'pancakeswap', 'orca'], - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), positionAddress: Type.String({ description: 'Position address or NFT token ID', examples: [''], @@ -33,24 +25,6 @@ const UnifiedPositionInfoRequestSchema = Type.Object({ type UnifiedPositionInfoRequest = Static; -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} - /** * Get position info from Solana connectors */ diff --git a/src/trading/clmm/quote-position.ts b/src/trading/clmm/quote-position.ts index d1a44ec22f..417c1d1076 100644 --- a/src/trading/clmm/quote-position.ts +++ b/src/trading/clmm/quote-position.ts @@ -1,8 +1,6 @@ import { Type, Static } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; import { quotePosition as meteoraQuotePosition } from '../../connectors/meteora/clmm-routes/quotePosition'; import { quotePosition as orcaQuotePosition } from '../../connectors/orca/clmm-routes/quotePosition'; import { quotePosition as pancakeswapQuotePosition } from '../../connectors/pancakeswap/clmm-routes/quotePosition'; @@ -12,6 +10,7 @@ import { quotePosition as uniswapQuotePosition } from '../../connectors/uniswap/ import { QuotePositionResponseType, QuotePositionResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; +import { CLMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; // Constants for examples (using Meteora CLMM values) const BASE_TOKEN_AMOUNT = 0.01; @@ -24,17 +23,8 @@ const CLMM_POOL_ADDRESS_EXAMPLE = '2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3' * Unified quote position request schema */ const UnifiedQuotePositionRequestSchema = Type.Object({ - connector: Type.String({ - description: 'CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap, orca)', - enum: ['raydium', 'meteora', 'pancakeswap-sol', 'uniswap', 'pancakeswap', 'orca'], - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), lowerPrice: Type.Number({ description: 'Lower price bound for the position', examples: [LOWER_PRICE_BOUND], @@ -72,24 +62,6 @@ const UnifiedQuotePositionRequestSchema = Type.Object({ type UnifiedQuotePositionRequest = Static; -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} - /** * Quote position from Solana connectors */ @@ -134,6 +106,7 @@ async function getSolanaQuotePosition( poolAddress, baseTokenAmount, quoteTokenAmount, + slippagePct, ); case 'orca': return await orcaQuotePosition( diff --git a/src/trading/common.ts b/src/trading/common.ts new file mode 100644 index 0000000000..b897bd09b5 --- /dev/null +++ b/src/trading/common.ts @@ -0,0 +1,43 @@ +import { Type } from '@sinclair/typebox'; + +import { getEthereumChainConfig } from '../chains/ethereum/ethereum.config'; +import { getSolanaChainConfig } from '../chains/solana/solana.config'; +import { httpErrors } from '../services/error-handler'; + +/** CLMM connectors that back the unified /trading/clmm routes. */ +export const CLMM_CONNECTORS = ['meteora', 'raydium', 'pancakeswap-sol', 'orca', 'uniswap', 'pancakeswap']; + +/** AMM connectors that back the unified /trading/amm routes. */ +export const AMM_CONNECTORS = ['meteora', 'raydium', 'uniswap', 'pancakeswap']; + +/** Connector selector: enum-constrained so unknown connectors are rejected at the schema. */ +export const connectorField = (connectors: string[], label: string) => + Type.String({ description: label, enum: connectors, default: connectors[0], examples: [connectors[0]] }); + +/** Chain-network selector shared by every unified trading route. */ +export const chainNetworkField = () => + Type.String({ + description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', + default: 'solana-mainnet-beta', + examples: ['solana-mainnet-beta'], + }); + +/** Parse a chain-network string (e.g. "solana-mainnet-beta") into its chain and network parts. */ +export function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { + const parts = chainNetwork.split('-'); + if (parts.length < 2) { + throw httpErrors.badRequest( + `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, + ); + } + return { chain: parts[0], network: parts.slice(1).join('-') }; +} + +// Default wallet from Solana config, falling back to Ethereum when Solana is unavailable. +let dw: string; +try { + dw = getSolanaChainConfig().defaultWallet; +} catch { + dw = getEthereumChainConfig().defaultWallet; +} +export const defaultWallet = dw; diff --git a/src/trading/swap/execute.ts b/src/trading/swap/execute.ts index 64d5ec5428..92a621bee6 100644 --- a/src/trading/swap/execute.ts +++ b/src/trading/swap/execute.ts @@ -2,8 +2,8 @@ import { Type, Static } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; // Solana connector imports -import { getEthereumChainConfig, getEthereumNetworkConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig, getSolanaNetworkConfig } from '../../chains/solana/solana.config'; +import { getEthereumNetworkConfig } from '../../chains/ethereum/ethereum.config'; +import { getSolanaNetworkConfig } from '../../chains/solana/solana.config'; import { executeSwap as zeroXRouterExecuteSwap } from '../../connectors/0x/router-routes/executeSwap'; import { executeSwap as dflowRouterExecuteSwap } from '../../connectors/dflow/router-routes/executeSwap'; import { executeSwap as jupiterRouterExecuteSwap } from '../../connectors/jupiter/router-routes/executeSwap'; @@ -28,16 +28,7 @@ import { ChainExecuteSwapResponseSchema } from '../../schemas/chain-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; import { PoolService } from '../../services/pool-service'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - const solanaChainConfig = getSolanaChainConfig(); - defaultWallet = solanaChainConfig.defaultWallet; -} catch { - const ethereumChainConfig = getEthereumChainConfig(); - defaultWallet = ethereumChainConfig.defaultWallet; -} +import { chainNetworkField, defaultWallet, parseChainNetwork } from '../common'; /** * Unified swap execute request schema @@ -48,11 +39,7 @@ const UnifiedExecuteSwapRequestSchema = Type.Object({ description: 'Wallet address to execute swap from', default: defaultWallet, }), - chainNetwork: Type.String({ - description: - 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet, ethereum-polygon)', - default: 'solana-mainnet-beta', - }), + chainNetwork: chainNetworkField(), connector: Type.Optional( Type.String({ description: @@ -94,30 +81,6 @@ const UnifiedExecuteSwapRequestSchema = Type.Object({ type UnifiedExecuteSwapRequest = Static; -/** - * Parse chain-network parameter into chain and network - * Examples: "solana-mainnet-beta" -> {chain: "solana", network: "mainnet-beta"} - * "ethereum-mainnet" -> {chain: "ethereum", network: "mainnet"} - * "ethereum-polygon" -> {chain: "ethereum", network: "polygon"} - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - // First part is always the chain - const chain = parts[0]; - - // Rest is the network (e.g., "mainnet-beta" from ["solana", "mainnet", "beta"]) - const network = parts.slice(1).join('-'); - - return { chain, network }; -} - /** * Execute a Solana swap */ diff --git a/src/trading/swap/quote.ts b/src/trading/swap/quote.ts index 565ebe3e7d..4fecc382ab 100644 --- a/src/trading/swap/quote.ts +++ b/src/trading/swap/quote.ts @@ -28,17 +28,14 @@ import { ChainQuoteSwapResponseSchema } from '../../schemas/chain-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; import { PoolService } from '../../services/pool-service'; +import { chainNetworkField, parseChainNetwork } from '../common'; /** * Unified swap quote request schema * Accepts chain-network parameter like "solana-mainnet-beta", "ethereum-mainnet", or "ethereum-polygon" */ const UnifiedQuoteSwapRequestSchema = Type.Object({ - chainNetwork: Type.String({ - description: - 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet, ethereum-polygon)', - default: 'solana-mainnet-beta', - }), + chainNetwork: chainNetworkField(), connector: Type.Optional( Type.String({ description: @@ -80,30 +77,6 @@ const UnifiedQuoteSwapRequestSchema = Type.Object({ type UnifiedQuoteSwapRequest = Static; -/** - * Parse chain-network parameter into chain and network - * Examples: "solana-mainnet-beta" -> {chain: "solana", network: "mainnet-beta"} - * "ethereum-mainnet" -> {chain: "ethereum", network: "mainnet"} - * "ethereum-polygon" -> {chain: "ethereum", network: "polygon"} - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw httpErrors.badRequest( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - // First part is always the chain - const chain = parts[0]; - - // Rest is the network (e.g., "mainnet-beta" from ["solana", "mainnet", "beta"]) - const network = parts.slice(1).join('-'); - - return { chain, network }; -} - /** * Get a Solana swap quote */ @@ -252,7 +225,7 @@ async function getEthereumQuoteSwap( const providerKey = swapProvider; if (providerKey === 'uniswap/router') { - return await uniswapRouterQuoteSwap(network, undefined, baseToken, quoteToken, amount, side, slippagePct || 1); + return await uniswapRouterQuoteSwap(network, undefined, baseToken, quoteToken, amount, side, slippagePct); } else if (providerKey === 'uniswap/amm') { return await uniswapAmmQuoteSwap(network, poolAddress!, baseToken, side, amount, slippagePct); } else if (providerKey === 'uniswap/clmm') { @@ -264,7 +237,7 @@ async function getEthereumQuoteSwap( } else if (providerKey === 'pancakeswap/clmm') { return await pancakeswapClmmQuoteSwap(network, poolAddress!, baseToken, side, amount, slippagePct); } else if (providerKey === '0x/router') { - return await zeroXRouterQuoteSwap(network, baseToken, quoteToken, amount, side, slippagePct || 1); + return await zeroXRouterQuoteSwap(network, baseToken, quoteToken, amount, side, slippagePct); } throw httpErrors.badRequest(`Unsupported swap provider: ${swapProvider}`); diff --git a/src/trading/trading-amm-routes/add-liquidity.ts b/src/trading/trading-amm-routes/add-liquidity.ts index f42c2209cf..3366fefdf2 100644 --- a/src/trading/trading-amm-routes/add-liquidity.ts +++ b/src/trading/trading-amm-routes/add-liquidity.ts @@ -8,15 +8,11 @@ import { addLiquidity as uniswapAddLiquidity } from '../../connectors/uniswap/am import { AddLiquidityResponse, AddLiquidityResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -import { AMM_CONNECTORS, parseChainNetwork, defaultWallet } from './common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; const UnifiedAmmAddLiquidityRequest = Type.Object({ - connector: Type.String({ description: 'AMM connector (meteora, raydium, uniswap)', default: 'meteora' }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet }), poolAddress: Type.String({ description: 'Pool contract address' }), baseTokenAmount: Type.Number({ description: 'Amount of base token to add' }), diff --git a/src/trading/trading-amm-routes/common.ts b/src/trading/trading-amm-routes/common.ts deleted file mode 100644 index a0e702f621..0000000000 --- a/src/trading/trading-amm-routes/common.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; - -/** AMM connectors that back the unified /trading/amm routes. */ -export const AMM_CONNECTORS = ['meteora', 'raydium', 'uniswap', 'pancakeswap']; - -/** Parse a chain-network string (e.g. "solana-mainnet-beta") into its chain and network parts. */ -export function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - return { chain: parts[0], network: parts.slice(1).join('-') }; -} - -// Default wallet from Solana config, falling back to Ethereum when Solana is unavailable. -let dw: string; -try { - dw = getSolanaChainConfig().defaultWallet; -} catch { - dw = getEthereumChainConfig().defaultWallet; -} -export const defaultWallet = dw; diff --git a/src/trading/trading-amm-routes/create-pool.ts b/src/trading/trading-amm-routes/create-pool.ts index bdaba9ada2..ee3eed51d5 100644 --- a/src/trading/trading-amm-routes/create-pool.ts +++ b/src/trading/trading-amm-routes/create-pool.ts @@ -1,8 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; import { createPool as meteoraCreatePool } from '../../connectors/meteora/amm-routes/createPool'; import { createPool as pancakeswapCreatePool } from '../../connectors/pancakeswap/amm-routes/createPool'; import { createPool as raydiumCreatePool } from '../../connectors/raydium/amm-routes/createPool'; @@ -10,45 +8,15 @@ import { createPool as uniswapCreatePool } from '../../connectors/uniswap/amm-ro import { CreatePoolRequest, CreatePoolResponse, CreatePoolResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - const solanaChainConfig = getSolanaChainConfig(); - defaultWallet = solanaChainConfig.defaultWallet; -} catch { - const ethereumChainConfig = getEthereumChainConfig(); - defaultWallet = ethereumChainConfig.defaultWallet; -} - -/** - * Parse chain-network parameter into chain and network. - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - return { chain: parts[0], network: parts.slice(1).join('-') }; -} +import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; // Composed from the canonical CreatePoolRequest (schemas/amm-schema.ts): the // unified route swaps per-connector `network` for connector + chainNetwork, // defaults the wallet, and adds the per-protocol fee-config selectors. const UnifiedCreatePoolRequest = Type.Composite([ Type.Object({ - connector: Type.String({ - description: 'AMM connector name (meteora, raydium, uniswap)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address (pool creator + payer)', default: defaultWallet, @@ -90,7 +58,8 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { { schema: { description: - 'Create and seed a new AMM pool across supported connectors (Meteora DAMM v2, Raydium CPMM, Uniswap V2)', + 'Create and seed a new AMM pool across supported connectors (Meteora DAMM v2, Raydium CPMM, ' + + 'Uniswap V2, PancakeSwap V2)', tags: ['/trading/amm'], body: UnifiedCreatePoolRequest, response: { diff --git a/src/trading/trading-amm-routes/execute-swap.ts b/src/trading/trading-amm-routes/execute-swap.ts index ac10ffae7d..fdab890f22 100644 --- a/src/trading/trading-amm-routes/execute-swap.ts +++ b/src/trading/trading-amm-routes/execute-swap.ts @@ -8,15 +8,11 @@ import { executeSwap as uniswapExecuteSwap } from '../../connectors/uniswap/amm- import { ExecuteSwapResponse, ExecuteSwapResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -import { AMM_CONNECTORS, parseChainNetwork, defaultWallet } from './common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; const UnifiedAmmExecuteSwapRequest = Type.Object({ - connector: Type.String({ description: 'AMM connector (meteora, raydium, uniswap)', default: 'meteora' }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet }), poolAddress: Type.String({ description: 'Pool contract address' }), baseToken: Type.String({ description: 'Base token symbol or address (determines swap direction)' }), diff --git a/src/trading/trading-amm-routes/pool-info.ts b/src/trading/trading-amm-routes/pool-info.ts index 2f553eb89b..e3667047d1 100644 --- a/src/trading/trading-amm-routes/pool-info.ts +++ b/src/trading/trading-amm-routes/pool-info.ts @@ -8,15 +8,11 @@ import { getPoolInfo as uniswapGetPoolInfo } from '../../connectors/uniswap/amm- import { PoolInfo, PoolInfoSchema } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -import { AMM_CONNECTORS, parseChainNetwork } from './common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; const UnifiedAmmPoolInfoRequest = Type.Object({ - connector: Type.String({ description: 'AMM connector (meteora, raydium, uniswap)', default: 'meteora' }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector'), + chainNetwork: chainNetworkField(), poolAddress: Type.String({ description: 'Pool contract address' }), }); diff --git a/src/trading/trading-amm-routes/position-info.ts b/src/trading/trading-amm-routes/position-info.ts index 8cde9b4c73..d183adb6db 100644 --- a/src/trading/trading-amm-routes/position-info.ts +++ b/src/trading/trading-amm-routes/position-info.ts @@ -8,15 +8,11 @@ import { getPositionInfo as uniswapGetPositionInfo } from '../../connectors/unis import { PositionInfo, PositionInfoSchema } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -import { AMM_CONNECTORS, parseChainNetwork, defaultWallet } from './common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; const UnifiedAmmPositionInfoRequest = Type.Object({ - connector: Type.String({ description: 'AMM connector (meteora, raydium, uniswap)', default: 'meteora' }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector'), + chainNetwork: chainNetworkField(), poolAddress: Type.String({ description: 'Pool contract address' }), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet }), }); diff --git a/src/trading/trading-amm-routes/positions-owned.ts b/src/trading/trading-amm-routes/positions-owned.ts index 43aec08d84..612d094c8b 100644 --- a/src/trading/trading-amm-routes/positions-owned.ts +++ b/src/trading/trading-amm-routes/positions-owned.ts @@ -5,15 +5,11 @@ import { getPositionsOwned as meteoraGetPositionsOwned } from '../../connectors/ import { PositionInfo, PositionInfoSchema } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -import { AMM_CONNECTORS, parseChainNetwork, defaultWallet } from './common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; const UnifiedAmmPositionsOwnedRequest = Type.Object({ - connector: Type.String({ description: 'AMM connector (meteora)', default: 'meteora' }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta)', - default: 'solana-mainnet-beta', - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector (only non-fungible-LP AMMs supported: meteora)'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address to list positions for', default: defaultWallet }), }); diff --git a/src/trading/trading-amm-routes/quote-liquidity.ts b/src/trading/trading-amm-routes/quote-liquidity.ts index 1d1989640c..1586bc0d16 100644 --- a/src/trading/trading-amm-routes/quote-liquidity.ts +++ b/src/trading/trading-amm-routes/quote-liquidity.ts @@ -8,15 +8,11 @@ import { quoteLiquidity as uniswapQuoteLiquidity } from '../../connectors/uniswa import { QuoteLiquidityResponse, QuoteLiquidityResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -import { AMM_CONNECTORS, parseChainNetwork } from './common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; const UnifiedAmmQuoteLiquidityRequest = Type.Object({ - connector: Type.String({ description: 'AMM connector (meteora, raydium, uniswap)', default: 'meteora' }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector'), + chainNetwork: chainNetworkField(), poolAddress: Type.String({ description: 'Pool contract address' }), baseTokenAmount: Type.Number({ description: 'Amount of base token to deposit' }), quoteTokenAmount: Type.Number({ description: 'Amount of quote token to deposit' }), diff --git a/src/trading/trading-amm-routes/quote-swap.ts b/src/trading/trading-amm-routes/quote-swap.ts index b2ea6f613e..9c7d278a33 100644 --- a/src/trading/trading-amm-routes/quote-swap.ts +++ b/src/trading/trading-amm-routes/quote-swap.ts @@ -8,15 +8,11 @@ import { quoteSwap as uniswapQuoteSwap } from '../../connectors/uniswap/amm-rout import { QuoteSwapResponse, QuoteSwapResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -import { AMM_CONNECTORS, parseChainNetwork } from './common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; const UnifiedAmmQuoteSwapRequest = Type.Object({ - connector: Type.String({ description: 'AMM connector (meteora, raydium, uniswap)', default: 'meteora' }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector'), + chainNetwork: chainNetworkField(), poolAddress: Type.String({ description: 'Pool contract address' }), baseToken: Type.String({ description: 'Base token symbol or address (determines swap direction)' }), amount: Type.Number({ description: 'Amount denominated in the base token' }), diff --git a/src/trading/trading-amm-routes/remove-liquidity.ts b/src/trading/trading-amm-routes/remove-liquidity.ts index eaee68647d..ed5df52c13 100644 --- a/src/trading/trading-amm-routes/remove-liquidity.ts +++ b/src/trading/trading-amm-routes/remove-liquidity.ts @@ -8,15 +8,11 @@ import { removeLiquidity as uniswapRemoveLiquidity } from '../../connectors/unis import { RemoveLiquidityResponse, RemoveLiquidityResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -import { AMM_CONNECTORS, parseChainNetwork, defaultWallet } from './common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; const UnifiedAmmRemoveLiquidityRequest = Type.Object({ - connector: Type.String({ description: 'AMM connector (meteora, raydium, uniswap)', default: 'meteora' }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - }), + connector: connectorField(AMM_CONNECTORS, 'AMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet }), poolAddress: Type.String({ description: 'Pool contract address' }), positionAddress: Type.Optional( @@ -26,7 +22,13 @@ const UnifiedAmmRemoveLiquidityRequest = Type.Object({ 'List positions with position-info or positions-owned. Ignored by fungible-LP AMMs.', }), ), - percentageToRemove: Type.Number({ minimum: 0, maximum: 100, description: 'Percentage of liquidity to remove' }), + percentageToRemove: Type.Number({ + minimum: 0, + maximum: 100, + description: 'Percentage of liquidity to remove', + default: 100, + examples: [100], + }), slippagePct: Type.Optional( Type.Number({ minimum: 0, diff --git a/src/trading/trading-clmm-routes/add.ts b/src/trading/trading-clmm-routes/add.ts index 5218d25174..4298245b51 100644 --- a/src/trading/trading-clmm-routes/add.ts +++ b/src/trading/trading-clmm-routes/add.ts @@ -1,8 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; import { addLiquidity as meteoraAddLiquidity } from '../../connectors/meteora/clmm-routes/addLiquidity'; import { addLiquidity as orcaAddLiquidity } from '../../connectors/orca/clmm-routes/addLiquidity'; import { addLiquidity as pancakeswapAddLiquidity } from '../../connectors/pancakeswap/clmm-routes/addLiquidity'; @@ -12,51 +10,16 @@ import { addLiquidity as uniswapAddLiquidity } from '../../connectors/uniswap/cl import { AddLiquidityResponseType, AddLiquidityResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - const solanaChainConfig = getSolanaChainConfig(); - defaultWallet = solanaChainConfig.defaultWallet; -} catch { - const ethereumChainConfig = getEthereumChainConfig(); - defaultWallet = ethereumChainConfig.defaultWallet; -} +import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; // Constants for examples (using Meteora CLMM values) const BASE_TOKEN_AMOUNT = 0.01; const QUOTE_TOKEN_AMOUNT = 2; -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} - // Unified schema with connector field const UnifiedAddLiquidityRequest = Type.Object({ - connector: Type.String({ - description: 'Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet, diff --git a/src/trading/trading-clmm-routes/close.ts b/src/trading/trading-clmm-routes/close.ts index 45b3469f73..043a5a0955 100644 --- a/src/trading/trading-clmm-routes/close.ts +++ b/src/trading/trading-clmm-routes/close.ts @@ -1,8 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; import { closePosition as meteoraClosePosition } from '../../connectors/meteora/clmm-routes/closePosition'; import { closePosition as orcaClosePosition } from '../../connectors/orca/clmm-routes/closePosition'; import { closePosition as pancakeswapClosePosition } from '../../connectors/pancakeswap/clmm-routes/closePosition'; @@ -12,47 +10,12 @@ import { closePosition as uniswapClosePosition } from '../../connectors/uniswap/ import { ClosePositionResponseType, ClosePositionResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - const solanaChainConfig = getSolanaChainConfig(); - defaultWallet = solanaChainConfig.defaultWallet; -} catch { - const ethereumChainConfig = getEthereumChainConfig(); - defaultWallet = ethereumChainConfig.defaultWallet; -} - -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} +import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; // Unified schema with connector field const UnifiedClosePositionRequest = Type.Object({ - connector: Type.String({ - description: 'Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet, diff --git a/src/trading/trading-clmm-routes/collect-fees.ts b/src/trading/trading-clmm-routes/collect-fees.ts index 8d0806e37f..22493eae7c 100644 --- a/src/trading/trading-clmm-routes/collect-fees.ts +++ b/src/trading/trading-clmm-routes/collect-fees.ts @@ -1,8 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; import { collectFees as meteoraCollectFees } from '../../connectors/meteora/clmm-routes/collectFees'; import { collectFees as orcaCollectFees } from '../../connectors/orca/clmm-routes/collectFees'; import { collectFees as pancakeswapCollectFees } from '../../connectors/pancakeswap/clmm-routes/collectFees'; @@ -12,47 +10,12 @@ import { collectFees as uniswapCollectFees } from '../../connectors/uniswap/clmm import { CollectFeesResponseType, CollectFeesResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - const solanaChainConfig = getSolanaChainConfig(); - defaultWallet = solanaChainConfig.defaultWallet; -} catch { - const ethereumChainConfig = getEthereumChainConfig(); - defaultWallet = ethereumChainConfig.defaultWallet; -} - -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} +import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; // Unified schema with connector field const UnifiedCollectFeesRequest = Type.Object({ - connector: Type.String({ - description: 'Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet, diff --git a/src/trading/trading-clmm-routes/create-pool.ts b/src/trading/trading-clmm-routes/create-pool.ts index 5f3ca2913f..15cfe46fbe 100644 --- a/src/trading/trading-clmm-routes/create-pool.ts +++ b/src/trading/trading-clmm-routes/create-pool.ts @@ -1,8 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; import { createPool as meteoraCreatePool } from '../../connectors/meteora/clmm-routes/createPool'; import { createPool as orcaCreatePool } from '../../connectors/orca/clmm-routes/createPool'; import { createPool as pancakeswapCreatePool } from '../../connectors/pancakeswap/clmm-routes/createPool'; @@ -16,24 +14,7 @@ import { } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - defaultWallet = getSolanaChainConfig().defaultWallet; -} catch { - defaultWallet = getEthereumChainConfig().defaultWallet; -} - -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - return { chain: parts[0], network: parts.slice(1).join('-') }; -} +import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; // Unified CLMM create-pool. Creates + initializes a pool at an initial price (no position is // seeded — concentrated-liquidity positions need a range, opened separately via open-position). @@ -43,16 +24,8 @@ function parseChainNetwork(chainNetwork: string): { chain: string; network: stri // and defaults the wallet. const UnifiedClmmCreatePoolRequest = Type.Composite([ Type.Object({ - connector: Type.String({ - description: 'CLMM connector name (meteora, raydium, uniswap, orca, pancakeswap, pancakeswap-sol)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address (pool creator + payer)', default: defaultWallet }), }), Type.Omit(ClmmCreatePoolRequest, ['network', 'walletAddress'], {}), @@ -67,7 +40,8 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { { schema: { description: - 'Create and initialize a new CLMM pool across supported connectors (Meteora DLMM, Raydium CLMM, Uniswap V3)', + 'Create and initialize a new CLMM pool across supported connectors (Meteora DLMM, Raydium CLMM, ' + + 'PancakeSwap Solana CLMM, Orca Whirlpool, Uniswap V3, PancakeSwap V3)', tags: ['/trading/clmm'], body: UnifiedClmmCreatePoolRequest, response: { 200: CreatePoolResponse }, diff --git a/src/trading/trading-clmm-routes/open.ts b/src/trading/trading-clmm-routes/open.ts index 934ef289af..318c06d8d4 100644 --- a/src/trading/trading-clmm-routes/open.ts +++ b/src/trading/trading-clmm-routes/open.ts @@ -1,22 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; -import { httpErrors } from '../../services/error-handler'; -import { Ethereum } from '../../chains/ethereum/ethereum'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { Solana } from '../../chains/solana/solana'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - const solanaChainConfig = getSolanaChainConfig(); - defaultWallet = solanaChainConfig.defaultWallet; -} catch { - const ethereumChainConfig = getEthereumChainConfig(); - defaultWallet = ethereumChainConfig.defaultWallet; -} - // Constants for examples (using Meteora CLMM values) const BASE_TOKEN_AMOUNT = 0.01; const QUOTE_TOKEN_AMOUNT = 2; @@ -24,36 +8,10 @@ const LOWER_PRICE_BOUND = 150; const UPPER_PRICE_BOUND = 250; const CLMM_POOL_ADDRESS_EXAMPLE = '2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3'; -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} - // Unified schema with connector field const UnifiedOpenPositionRequest = Type.Object({ - connector: Type.String({ - description: 'Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet, @@ -108,7 +66,9 @@ import { openPosition as pancakeswapSolOpenPosition } from '../../connectors/pan import { openPosition as raydiumOpenPosition } from '../../connectors/raydium/clmm-routes/openPosition'; import { openPosition as uniswapOpenPosition } from '../../connectors/uniswap/clmm-routes/openPosition'; import { OpenPositionResponseType, OpenPositionResponse } from '../../schemas/clmm-schema'; +import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; +import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; export const openPositionRoute: FastifyPluginAsync = async (fastify) => { fastify.post<{ diff --git a/src/trading/trading-clmm-routes/remove.ts b/src/trading/trading-clmm-routes/remove.ts index f76b555603..28ce999412 100644 --- a/src/trading/trading-clmm-routes/remove.ts +++ b/src/trading/trading-clmm-routes/remove.ts @@ -1,8 +1,6 @@ import { Static, Type } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; -import { getEthereumChainConfig } from '../../chains/ethereum/ethereum.config'; -import { getSolanaChainConfig } from '../../chains/solana/solana.config'; import { removeLiquidity as meteoraRemoveLiquidity } from '../../connectors/meteora/clmm-routes/removeLiquidity'; import { removeLiquidity as orcaRemoveLiquidity } from '../../connectors/orca/clmm-routes/removeLiquidity'; import { removeLiquidity as pancakeswapRemoveLiquidity } from '../../connectors/pancakeswap/clmm-routes/removeLiquidity'; @@ -12,47 +10,12 @@ import { removeLiquidity as uniswapRemoveLiquidity } from '../../connectors/unis import { RemoveLiquidityResponseType, RemoveLiquidityResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; - -// Get default wallet from Solana config, fallback to Ethereum if Solana doesn't exist -let defaultWallet: string; -try { - const solanaChainConfig = getSolanaChainConfig(); - defaultWallet = solanaChainConfig.defaultWallet; -} catch { - const ethereumChainConfig = getEthereumChainConfig(); - defaultWallet = ethereumChainConfig.defaultWallet; -} - -/** - * Parse chain-network parameter into chain and network - */ -function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { - const parts = chainNetwork.split('-'); - - if (parts.length < 2) { - throw new Error( - `Invalid chain-network format: ${chainNetwork}. Expected format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)`, - ); - } - - const chain = parts[0]; - const network = parts.slice(1).join('-'); - - return { chain, network }; -} +import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; // Unified schema with connector field const UnifiedRemoveLiquidityRequest = Type.Object({ - connector: Type.String({ - description: 'Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)', - default: 'meteora', - examples: ['meteora'], - }), - chainNetwork: Type.String({ - description: 'Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)', - default: 'solana-mainnet-beta', - examples: ['solana-mainnet-beta'], - }), + connector: connectorField(CLMM_CONNECTORS, 'CLMM connector'), + chainNetwork: chainNetworkField(), walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet, @@ -68,6 +31,17 @@ const UnifiedRemoveLiquidityRequest = Type.Object({ default: 100, examples: [100], }), + // Orca-specific parameter (optional, ignored by other connectors, which manage + // slippage internally). + slippagePct: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'Maximum acceptable slippage percentage. Only applies to the Orca connector.', + default: 1, + examples: [1], + }), + ), }); // Import connector functions @@ -90,7 +64,8 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { connector, chainNetwork, walletAddress, positionAddress, percentageToRemove } = request.body; + const { connector, chainNetwork, walletAddress, positionAddress, percentageToRemove, slippagePct } = + request.body; // Parse chain and network from chainNetwork parameter const { network } = parseChainNetwork(chainNetwork); @@ -113,7 +88,13 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { return await pancakeswapSolRemoveLiquidity(network, walletAddress, positionAddress, percentageToRemove); case 'orca': - return await orcaRemoveLiquidity(network, walletAddress, positionAddress, percentageToRemove, 1); + return await orcaRemoveLiquidity( + network, + walletAddress, + positionAddress, + percentageToRemove, + slippagePct ?? 1, + ); default: throw httpErrors.badRequest(`Unsupported connector: ${connector}`); diff --git a/src/trading/trading.routes.ts b/src/trading/trading.routes.ts index b892c6a5f3..414d2132cf 100644 --- a/src/trading/trading.routes.ts +++ b/src/trading/trading.routes.ts @@ -69,8 +69,3 @@ export const tradingAmmRoutes: FastifyPluginAsync = async (fastify) => { fastify.register(ammRemoveLiquidityRoute); fastify.register(createPoolRoute); }; - -// Legacy export for backward compatibility -export const tradingRoutes = tradingSwapRoutes; - -export default tradingRoutes; diff --git a/test/trading/clmm/routes.test.ts b/test/trading/clmm/routes.test.ts index 8fabe59197..382cf7c543 100644 --- a/test/trading/clmm/routes.test.ts +++ b/test/trading/clmm/routes.test.ts @@ -365,7 +365,8 @@ describe('Unified Trading CLMM Routes', () => { expect(response.statusCode).toBe(400); const body = JSON.parse(response.body); - expect(body.message).toContain('Unsupported connector'); + // The connector field is enum-constrained, so rejection happens at schema validation. + expect(body.message).toContain('must be equal to one of the allowed values'); }); }); }); diff --git a/test/trading/trading-amm-routes/create-pool.test.ts b/test/trading/trading-amm-routes/create-pool.test.ts index a100a49ba3..4de0d8ea1a 100644 --- a/test/trading/trading-amm-routes/create-pool.test.ts +++ b/test/trading/trading-amm-routes/create-pool.test.ts @@ -34,6 +34,6 @@ describe('POST /trading/amm/create-pool (unified dispatch)', () => { }); expect(response.statusCode).toBe(400); - expect(JSON.parse(response.body).message).toMatch(/Unsupported AMM connector/); + expect(JSON.parse(response.body).message).toMatch(/must be equal to one of the allowed values/); }); }); diff --git a/test/trading/trading-amm-routes/positions-owned.test.ts b/test/trading/trading-amm-routes/positions-owned.test.ts index 73504851cb..20e1d96993 100644 --- a/test/trading/trading-amm-routes/positions-owned.test.ts +++ b/test/trading/trading-amm-routes/positions-owned.test.ts @@ -41,6 +41,6 @@ describe('GET /trading/amm/positions-owned (unified dispatch)', () => { }); expect(response.statusCode).toBe(400); - expect(JSON.parse(response.body).message).toMatch(/Unsupported AMM connector/); + expect(JSON.parse(response.body).message).toMatch(/must be equal to one of the allowed values/); }); }); diff --git a/test/trading/trading-amm-routes/remove-liquidity.test.ts b/test/trading/trading-amm-routes/remove-liquidity.test.ts index 26f6791b4f..bf343b4ff5 100644 --- a/test/trading/trading-amm-routes/remove-liquidity.test.ts +++ b/test/trading/trading-amm-routes/remove-liquidity.test.ts @@ -50,6 +50,6 @@ describe('POST /trading/amm/remove-liquidity (unified dispatch)', () => { }); expect(response.statusCode).toBe(400); - expect(JSON.parse(response.body).message).toMatch(/Unsupported AMM connector/); + expect(JSON.parse(response.body).message).toMatch(/must be equal to one of the allowed values/); }); }); diff --git a/test/trading/trading-clmm-routes/create-pool.test.ts b/test/trading/trading-clmm-routes/create-pool.test.ts index 4c1da80f74..0871f879a8 100644 --- a/test/trading/trading-clmm-routes/create-pool.test.ts +++ b/test/trading/trading-clmm-routes/create-pool.test.ts @@ -33,6 +33,6 @@ describe('POST /trading/clmm/create-pool (unified dispatch)', () => { }); expect(response.statusCode).toBe(400); - expect(JSON.parse(response.body).message).toMatch(/Unsupported CLMM connector/); + expect(JSON.parse(response.body).message).toMatch(/must be equal to one of the allowed values/); }); }); From 26c3bb36e59e22be3b73b868ed01616c75fcb5a4 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 17:52:11 -0700 Subject: [PATCH 20/31] fix(trading): uniform error passthrough on all unified routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/trading/clmm/pools.ts | 5 ++- src/trading/clmm/positions-owned.ts | 12 +++++-- src/trading/clmm/positions.ts | 5 ++- src/trading/clmm/quote-position.ts | 5 ++- src/trading/common.ts | 15 ++++++++ src/trading/swap/execute.ts | 8 ++--- src/trading/swap/quote.ts | 8 ++--- .../trading-amm-routes/add-liquidity.ts | 14 +++++--- src/trading/trading-amm-routes/create-pool.ts | 14 +++++--- .../trading-amm-routes/execute-swap.ts | 14 +++++--- src/trading/trading-amm-routes/pool-info.ts | 7 ++-- .../trading-amm-routes/position-info.ts | 14 +++++--- .../trading-amm-routes/positions-owned.ts | 14 +++++--- .../trading-amm-routes/quote-liquidity.ts | 7 ++-- src/trading/trading-amm-routes/quote-swap.ts | 7 ++-- .../trading-amm-routes/remove-liquidity.ts | 14 +++++--- src/trading/trading-clmm-routes/add.ts | 16 +++++---- src/trading/trading-clmm-routes/close.ts | 16 +++++---- .../trading-clmm-routes/collect-fees.ts | 16 +++++---- .../trading-clmm-routes/create-pool.ts | 14 +++++--- src/trading/trading-clmm-routes/open.ts | 35 ++++++++++--------- src/trading/trading-clmm-routes/remove.ts | 16 +++++---- 22 files changed, 157 insertions(+), 119 deletions(-) diff --git a/src/trading/clmm/pools.ts b/src/trading/clmm/pools.ts index eb9c5d392f..d8eaea3353 100644 --- a/src/trading/clmm/pools.ts +++ b/src/trading/clmm/pools.ts @@ -11,7 +11,7 @@ import { getPoolInfo as raydiumGetPoolInfo } from '../../connectors/raydium/clmm import { getPoolInfo as uniswapGetPoolInfo } from '../../connectors/uniswap/clmm-routes/poolInfo'; import { PoolInfo, PoolInfoSchema } from '../../schemas/clmm-schema'; import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; +import { chainNetworkField, CLMM_CONNECTORS, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; // Constants for examples (using Meteora CLMM values) const CLMM_POOL_ADDRESS_EXAMPLE = '2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3'; @@ -143,8 +143,7 @@ export const poolsRoute: FastifyPluginAsync = async (fastify) => { const result = await getUnifiedPoolInfo(fastify, connector, chainNetwork, poolAddress, binCount); return reply.code(200).send(result); } catch (error: any) { - logger.error(`[UnifiedCLMM] Pool info error: ${error.message}`); - throw error; + rethrowRouteError(error, 'Failed to get CLMM pool info'); } }, ); diff --git a/src/trading/clmm/positions-owned.ts b/src/trading/clmm/positions-owned.ts index 6263c835c3..1c8d47199c 100644 --- a/src/trading/clmm/positions-owned.ts +++ b/src/trading/clmm/positions-owned.ts @@ -9,7 +9,14 @@ import { getPositionsOwned as raydiumGetPositionsOwned } from '../../connectors/ import { getPositionsOwned as uniswapGetPositionsOwned } from '../../connectors/uniswap/clmm-routes/positionsOwned'; import { PositionInfo, PositionInfoSchema } from '../../schemas/clmm-schema'; import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + chainNetworkField, + CLMM_CONNECTORS, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; /** * Unified positions owned request schema @@ -123,8 +130,7 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { const result = await getUnifiedPositionsOwned(fastify, connector, chainNetwork, walletAddress); return reply.code(200).send(result); } catch (error: any) { - logger.error(`[UnifiedCLMM] Positions owned error: ${error.message}`); - throw error; + rethrowRouteError(error, 'Failed to list CLMM positions owned'); } }, ); diff --git a/src/trading/clmm/positions.ts b/src/trading/clmm/positions.ts index 9f969b6740..dcee78ba1c 100644 --- a/src/trading/clmm/positions.ts +++ b/src/trading/clmm/positions.ts @@ -9,7 +9,7 @@ import { getPositionInfo as raydiumGetPositionInfo } from '../../connectors/rayd import { getPositionInfo as uniswapGetPositionInfo } from '../../connectors/uniswap/clmm-routes/positionInfo'; import { PositionInfo, PositionInfoSchema } from '../../schemas/clmm-schema'; import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; +import { chainNetworkField, CLMM_CONNECTORS, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; /** * Unified position info request schema @@ -123,8 +123,7 @@ export const positionsRoute: FastifyPluginAsync = async (fastify) => { const result = await getUnifiedPositionInfo(fastify, connector, chainNetwork, positionAddress); return reply.code(200).send(result); } catch (error: any) { - logger.error(`[UnifiedCLMM] Position info error: ${error.message}`); - throw error; + rethrowRouteError(error, 'Failed to get CLMM position info'); } }, ); diff --git a/src/trading/clmm/quote-position.ts b/src/trading/clmm/quote-position.ts index 417c1d1076..4cd2ecfa01 100644 --- a/src/trading/clmm/quote-position.ts +++ b/src/trading/clmm/quote-position.ts @@ -10,7 +10,7 @@ import { quotePosition as uniswapQuotePosition } from '../../connectors/uniswap/ import { QuotePositionResponseType, QuotePositionResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; +import { chainNetworkField, CLMM_CONNECTORS, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; // Constants for examples (using Meteora CLMM values) const BASE_TOKEN_AMOUNT = 0.01; @@ -256,8 +256,7 @@ export const quotePositionRoute: FastifyPluginAsync = async (fastify) => { ); return reply.code(200).send(result); } catch (error: any) { - logger.error(`[UnifiedCLMM] Quote position error: ${error.message}`); - throw error; + rethrowRouteError(error, 'Failed to quote CLMM position'); } }, ); diff --git a/src/trading/common.ts b/src/trading/common.ts index b897bd09b5..75f82a9665 100644 --- a/src/trading/common.ts +++ b/src/trading/common.ts @@ -3,6 +3,7 @@ import { Type } from '@sinclair/typebox'; import { getEthereumChainConfig } from '../chains/ethereum/ethereum.config'; import { getSolanaChainConfig } from '../chains/solana/solana.config'; import { httpErrors } from '../services/error-handler'; +import { logger } from '../services/logger'; /** CLMM connectors that back the unified /trading/clmm routes. */ export const CLMM_CONNECTORS = ['meteora', 'raydium', 'pancakeswap-sol', 'orca', 'uniswap', 'pancakeswap']; @@ -22,6 +23,20 @@ export const chainNetworkField = () => examples: ['solana-mainnet-beta'], }); +/** + * Standard catch handler for the unified trading routes: errors that already + * carry an HTTP status code (connector badRequest/notFound, chain errors) pass + * through untouched; anything else becomes a 500 that keeps the underlying + * message so callers see the real cause instead of a generic label. + */ +export function rethrowRouteError(e: any, context: string): never { + logger.error(`${context}: ${e?.message ?? e}`); + if (e?.statusCode) { + throw e; + } + throw httpErrors.internalServerError(`${context}: ${e?.message ?? e}`); +} + /** Parse a chain-network string (e.g. "solana-mainnet-beta") into its chain and network parts. */ export function parseChainNetwork(chainNetwork: string): { chain: string; network: string } { const parts = chainNetwork.split('-'); diff --git a/src/trading/swap/execute.ts b/src/trading/swap/execute.ts index 92a621bee6..833dad8b1f 100644 --- a/src/trading/swap/execute.ts +++ b/src/trading/swap/execute.ts @@ -28,7 +28,7 @@ import { ChainExecuteSwapResponseSchema } from '../../schemas/chain-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; import { PoolService } from '../../services/pool-service'; -import { chainNetworkField, defaultWallet, parseChainNetwork } from '../common'; +import { chainNetworkField, defaultWallet, parseChainNetwork, rethrowRouteError } from '../common'; /** * Unified swap execute request schema @@ -378,11 +378,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { ); return reply.code(200).send(result); } catch (error: any) { - logger.error(`[UnifiedSwap] Execute error: ${error.message}`); - if (error.statusCode) { - throw error; - } - throw fastify.httpErrors.internalServerError(error.message || 'Failed to execute swap'); + rethrowRouteError(error, 'Failed to execute swap'); } }, ); diff --git a/src/trading/swap/quote.ts b/src/trading/swap/quote.ts index 4fecc382ab..7a3cfefd68 100644 --- a/src/trading/swap/quote.ts +++ b/src/trading/swap/quote.ts @@ -28,7 +28,7 @@ import { ChainQuoteSwapResponseSchema } from '../../schemas/chain-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; import { PoolService } from '../../services/pool-service'; -import { chainNetworkField, parseChainNetwork } from '../common'; +import { chainNetworkField, parseChainNetwork, rethrowRouteError } from '../common'; /** * Unified swap quote request schema @@ -324,11 +324,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { ); return reply.code(200).send(result); } catch (error: any) { - logger.error(`[UnifiedSwap] Quote error: ${error.message}`); - if (error.statusCode) { - throw error; - } - throw fastify.httpErrors.internalServerError(error.message || 'Failed to get swap quote'); + rethrowRouteError(error, 'Failed to get swap quote'); } }, ); diff --git a/src/trading/trading-amm-routes/add-liquidity.ts b/src/trading/trading-amm-routes/add-liquidity.ts index 3366fefdf2..8895ee5f83 100644 --- a/src/trading/trading-amm-routes/add-liquidity.ts +++ b/src/trading/trading-amm-routes/add-liquidity.ts @@ -7,8 +7,14 @@ import { addLiquidity as raydiumAddLiquidity } from '../../connectors/raydium/am import { addLiquidity as uniswapAddLiquidity } from '../../connectors/uniswap/amm-routes/addLiquidity'; import { AddLiquidityResponse, AddLiquidityResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + AMM_CONNECTORS, + chainNetworkField, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; const UnifiedAmmAddLiquidityRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -106,9 +112,7 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { ); } } catch (e: any) { - logger.error('Failed to add AMM liquidity:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to add liquidity'); + rethrowRouteError(e, 'Failed to add AMM liquidity'); } }, ); diff --git a/src/trading/trading-amm-routes/create-pool.ts b/src/trading/trading-amm-routes/create-pool.ts index ee3eed51d5..4c3d1d5d5c 100644 --- a/src/trading/trading-amm-routes/create-pool.ts +++ b/src/trading/trading-amm-routes/create-pool.ts @@ -7,8 +7,14 @@ import { createPool as raydiumCreatePool } from '../../connectors/raydium/amm-ro import { createPool as uniswapCreatePool } from '../../connectors/uniswap/amm-routes/createPool'; import { CreatePoolRequest, CreatePoolResponse, CreatePoolResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + AMM_CONNECTORS, + chainNetworkField, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; // Composed from the canonical CreatePoolRequest (schemas/amm-schema.ts): the // unified route swaps per-connector `network` for connector + chainNetwork, @@ -138,9 +144,7 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { throw httpErrors.badRequest(`Unsupported AMM connector: ${connector}`); } } catch (e: any) { - logger.error('Failed to create pool:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to create pool'); + rethrowRouteError(e, 'Failed to create pool'); } }, ); diff --git a/src/trading/trading-amm-routes/execute-swap.ts b/src/trading/trading-amm-routes/execute-swap.ts index fdab890f22..9d7146b0cf 100644 --- a/src/trading/trading-amm-routes/execute-swap.ts +++ b/src/trading/trading-amm-routes/execute-swap.ts @@ -7,8 +7,14 @@ import { executeSwap as raydiumExecuteSwap } from '../../connectors/raydium/amm- import { executeSwap as uniswapExecuteSwap } from '../../connectors/uniswap/amm-routes/executeSwap'; import { ExecuteSwapResponse, ExecuteSwapResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + AMM_CONNECTORS, + chainNetworkField, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; const UnifiedAmmExecuteSwapRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -64,9 +70,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { ); } } catch (e: any) { - logger.error('Failed to execute AMM swap:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to execute swap'); + rethrowRouteError(e, 'Failed to execute AMM swap'); } }, ); diff --git a/src/trading/trading-amm-routes/pool-info.ts b/src/trading/trading-amm-routes/pool-info.ts index e3667047d1..de8b90f6c0 100644 --- a/src/trading/trading-amm-routes/pool-info.ts +++ b/src/trading/trading-amm-routes/pool-info.ts @@ -7,8 +7,7 @@ import { getPoolInfo as raydiumGetPoolInfo } from '../../connectors/raydium/amm- import { getPoolInfo as uniswapGetPoolInfo } from '../../connectors/uniswap/amm-routes/poolInfo'; import { PoolInfo, PoolInfoSchema } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; const UnifiedAmmPoolInfoRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -49,9 +48,7 @@ export const poolInfoRoute: FastifyPluginAsync = async (fastify) => { ); } } catch (e: any) { - logger.error('Failed to get AMM pool info:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to get pool info'); + rethrowRouteError(e, 'Failed to get AMM pool info'); } }, ); diff --git a/src/trading/trading-amm-routes/position-info.ts b/src/trading/trading-amm-routes/position-info.ts index d183adb6db..fdd0c84b1e 100644 --- a/src/trading/trading-amm-routes/position-info.ts +++ b/src/trading/trading-amm-routes/position-info.ts @@ -7,8 +7,14 @@ import { getPositionInfo as raydiumGetPositionInfo } from '../../connectors/rayd import { getPositionInfo as uniswapGetPositionInfo } from '../../connectors/uniswap/amm-routes/positionInfo'; import { PositionInfo, PositionInfoSchema } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + AMM_CONNECTORS, + chainNetworkField, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; const UnifiedAmmPositionInfoRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -50,9 +56,7 @@ export const positionInfoRoute: FastifyPluginAsync = async (fastify) => { ); } } catch (e: any) { - logger.error('Failed to get AMM position info:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to get position info'); + rethrowRouteError(e, 'Failed to get AMM position info'); } }, ); diff --git a/src/trading/trading-amm-routes/positions-owned.ts b/src/trading/trading-amm-routes/positions-owned.ts index 612d094c8b..6b0ceb0033 100644 --- a/src/trading/trading-amm-routes/positions-owned.ts +++ b/src/trading/trading-amm-routes/positions-owned.ts @@ -4,8 +4,14 @@ import { FastifyPluginAsync } from 'fastify'; import { getPositionsOwned as meteoraGetPositionsOwned } from '../../connectors/meteora/amm-routes/positionsOwned'; import { PositionInfo, PositionInfoSchema } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + AMM_CONNECTORS, + chainNetworkField, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; const UnifiedAmmPositionsOwnedRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector (only non-fungible-LP AMMs supported: meteora)'), @@ -50,9 +56,7 @@ export const positionsOwnedRoute: FastifyPluginAsync = async (fastify) => { ); } } catch (e: any) { - logger.error('Failed to list AMM positions owned:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to list positions owned'); + rethrowRouteError(e, 'Failed to list AMM positions owned'); } }, ); diff --git a/src/trading/trading-amm-routes/quote-liquidity.ts b/src/trading/trading-amm-routes/quote-liquidity.ts index 1586bc0d16..a3cf6080a8 100644 --- a/src/trading/trading-amm-routes/quote-liquidity.ts +++ b/src/trading/trading-amm-routes/quote-liquidity.ts @@ -7,8 +7,7 @@ import { quoteLiquidity as raydiumQuoteLiquidity } from '../../connectors/raydiu import { quoteLiquidity as uniswapQuoteLiquidity } from '../../connectors/uniswap/amm-routes/quoteLiquidity'; import { QuoteLiquidityResponse, QuoteLiquidityResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; const UnifiedAmmQuoteLiquidityRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -66,9 +65,7 @@ export const quoteLiquidityRoute: FastifyPluginAsync = async (fastify) => { ); } } catch (e: any) { - logger.error('Failed to quote AMM liquidity:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to quote liquidity'); + rethrowRouteError(e, 'Failed to quote AMM liquidity'); } }, ); diff --git a/src/trading/trading-amm-routes/quote-swap.ts b/src/trading/trading-amm-routes/quote-swap.ts index 9c7d278a33..887c340f2e 100644 --- a/src/trading/trading-amm-routes/quote-swap.ts +++ b/src/trading/trading-amm-routes/quote-swap.ts @@ -7,8 +7,7 @@ import { quoteSwap as raydiumQuoteSwap } from '../../connectors/raydium/amm-rout import { quoteSwap as uniswapQuoteSwap } from '../../connectors/uniswap/amm-routes/quoteSwap'; import { QuoteSwapResponse, QuoteSwapResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork } from '../common'; +import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; const UnifiedAmmQuoteSwapRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -62,9 +61,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { ); } } catch (e: any) { - logger.error('Failed to get AMM swap quote:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to get swap quote'); + rethrowRouteError(e, 'Failed to get AMM swap quote'); } }, ); diff --git a/src/trading/trading-amm-routes/remove-liquidity.ts b/src/trading/trading-amm-routes/remove-liquidity.ts index ed5df52c13..75475db28e 100644 --- a/src/trading/trading-amm-routes/remove-liquidity.ts +++ b/src/trading/trading-amm-routes/remove-liquidity.ts @@ -7,8 +7,14 @@ import { removeLiquidity as raydiumRemoveLiquidity } from '../../connectors/rayd import { removeLiquidity as uniswapRemoveLiquidity } from '../../connectors/uniswap/amm-routes/removeLiquidity'; import { RemoveLiquidityResponse, RemoveLiquidityResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + AMM_CONNECTORS, + chainNetworkField, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; const UnifiedAmmRemoveLiquidityRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -100,9 +106,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { ); } } catch (e: any) { - logger.error('Failed to remove AMM liquidity:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to remove liquidity'); + rethrowRouteError(e, 'Failed to remove AMM liquidity'); } }, ); diff --git a/src/trading/trading-clmm-routes/add.ts b/src/trading/trading-clmm-routes/add.ts index 4298245b51..64a5ff6c95 100644 --- a/src/trading/trading-clmm-routes/add.ts +++ b/src/trading/trading-clmm-routes/add.ts @@ -9,8 +9,14 @@ import { addLiquidity as raydiumAddLiquidity } from '../../connectors/raydium/cl import { addLiquidity as uniswapAddLiquidity } from '../../connectors/uniswap/clmm-routes/addLiquidity'; import { AddLiquidityResponseType, AddLiquidityResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + chainNetworkField, + CLMM_CONNECTORS, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; // Constants for examples (using Meteora CLMM values) const BASE_TOKEN_AMOUNT = 0.01; @@ -168,11 +174,7 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { throw httpErrors.badRequest(`Unsupported connector: ${connector}`); } } catch (e: any) { - logger.error('Failed to add liquidity:', e); - if (e.statusCode) { - throw e; - } - throw httpErrors.internalServerError('Failed to add liquidity'); + rethrowRouteError(e, 'Failed to add liquidity'); } }, ); diff --git a/src/trading/trading-clmm-routes/close.ts b/src/trading/trading-clmm-routes/close.ts index 043a5a0955..3a5904ad38 100644 --- a/src/trading/trading-clmm-routes/close.ts +++ b/src/trading/trading-clmm-routes/close.ts @@ -9,8 +9,14 @@ import { closePosition as raydiumClosePosition } from '../../connectors/raydium/ import { closePosition as uniswapClosePosition } from '../../connectors/uniswap/clmm-routes/closePosition'; import { ClosePositionResponseType, ClosePositionResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + chainNetworkField, + CLMM_CONNECTORS, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; // Unified schema with connector field const UnifiedClosePositionRequest = Type.Object({ @@ -75,11 +81,7 @@ export const closePositionRoute: FastifyPluginAsync = async (fastify) => { throw httpErrors.badRequest(`Unsupported connector: ${connector}`); } } catch (e: any) { - logger.error('Failed to close position:', e); - if (e.statusCode) { - throw e; - } - throw httpErrors.internalServerError('Failed to close position'); + rethrowRouteError(e, 'Failed to close position'); } }, ); diff --git a/src/trading/trading-clmm-routes/collect-fees.ts b/src/trading/trading-clmm-routes/collect-fees.ts index 22493eae7c..5c08de9e52 100644 --- a/src/trading/trading-clmm-routes/collect-fees.ts +++ b/src/trading/trading-clmm-routes/collect-fees.ts @@ -9,8 +9,14 @@ import { collectFees as raydiumCollectFees } from '../../connectors/raydium/clmm import { collectFees as uniswapCollectFees } from '../../connectors/uniswap/clmm-routes/collectFees'; import { CollectFeesResponseType, CollectFeesResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + chainNetworkField, + CLMM_CONNECTORS, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; // Unified schema with connector field const UnifiedCollectFeesRequest = Type.Object({ @@ -75,11 +81,7 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { throw httpErrors.badRequest(`Unsupported connector: ${connector}`); } } catch (e: any) { - logger.error('Failed to collect fees:', e); - if (e.statusCode) { - throw e; - } - throw httpErrors.internalServerError('Failed to collect fees'); + rethrowRouteError(e, 'Failed to collect fees'); } }, ); diff --git a/src/trading/trading-clmm-routes/create-pool.ts b/src/trading/trading-clmm-routes/create-pool.ts index 15cfe46fbe..de5d829321 100644 --- a/src/trading/trading-clmm-routes/create-pool.ts +++ b/src/trading/trading-clmm-routes/create-pool.ts @@ -13,8 +13,14 @@ import { CreatePoolRequest as ClmmCreatePoolRequest, } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + chainNetworkField, + CLMM_CONNECTORS, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; // Unified CLMM create-pool. Creates + initializes a pool at an initial price (no position is // seeded — concentrated-liquidity positions need a range, opened separately via open-position). @@ -107,9 +113,7 @@ export const createPoolRoute: FastifyPluginAsync = async (fastify) => { throw httpErrors.badRequest(`Unsupported CLMM connector: ${connector}`); } } catch (e: any) { - logger.error('Failed to create CLMM pool:', e); - if (e.statusCode) throw e; - throw httpErrors.internalServerError('Failed to create pool'); + rethrowRouteError(e, 'Failed to create CLMM pool'); } }, ); diff --git a/src/trading/trading-clmm-routes/open.ts b/src/trading/trading-clmm-routes/open.ts index 318c06d8d4..fff88da12e 100644 --- a/src/trading/trading-clmm-routes/open.ts +++ b/src/trading/trading-clmm-routes/open.ts @@ -1,6 +1,23 @@ import { Static, Type } from '@sinclair/typebox'; import { FastifyPluginAsync } from 'fastify'; +import { openPosition as meteoraOpenPosition } from '../../connectors/meteora/clmm-routes/openPosition'; +import { openPosition as orcaOpenPosition } from '../../connectors/orca/clmm-routes/openPosition'; +import { openPosition as pancakeswapOpenPosition } from '../../connectors/pancakeswap/clmm-routes/openPosition'; +import { openPosition as pancakeswapSolOpenPosition } from '../../connectors/pancakeswap-sol/clmm-routes/openPosition'; +import { openPosition as raydiumOpenPosition } from '../../connectors/raydium/clmm-routes/openPosition'; +import { openPosition as uniswapOpenPosition } from '../../connectors/uniswap/clmm-routes/openPosition'; +import { OpenPositionResponseType, OpenPositionResponse } from '../../schemas/clmm-schema'; +import { httpErrors } from '../../services/error-handler'; +import { + chainNetworkField, + CLMM_CONNECTORS, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; + // Constants for examples (using Meteora CLMM values) const BASE_TOKEN_AMOUNT = 0.01; const QUOTE_TOKEN_AMOUNT = 2; @@ -58,18 +75,6 @@ const UnifiedOpenPositionRequest = Type.Object({ ), }); -// Import connector functions -import { openPosition as meteoraOpenPosition } from '../../connectors/meteora/clmm-routes/openPosition'; -import { openPosition as orcaOpenPosition } from '../../connectors/orca/clmm-routes/openPosition'; -import { openPosition as pancakeswapOpenPosition } from '../../connectors/pancakeswap/clmm-routes/openPosition'; -import { openPosition as pancakeswapSolOpenPosition } from '../../connectors/pancakeswap-sol/clmm-routes/openPosition'; -import { openPosition as raydiumOpenPosition } from '../../connectors/raydium/clmm-routes/openPosition'; -import { openPosition as uniswapOpenPosition } from '../../connectors/uniswap/clmm-routes/openPosition'; -import { OpenPositionResponseType, OpenPositionResponse } from '../../schemas/clmm-schema'; -import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; - export const openPositionRoute: FastifyPluginAsync = async (fastify) => { fastify.post<{ Body: Static; @@ -191,11 +196,7 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { throw httpErrors.badRequest(`Unsupported connector: ${connector}`); } } catch (e: any) { - logger.error('Failed to open position:', e); - if (e.statusCode) { - throw e; - } - throw httpErrors.internalServerError('Failed to open position'); + rethrowRouteError(e, 'Failed to open position'); } }, ); diff --git a/src/trading/trading-clmm-routes/remove.ts b/src/trading/trading-clmm-routes/remove.ts index 28ce999412..d491ca3f77 100644 --- a/src/trading/trading-clmm-routes/remove.ts +++ b/src/trading/trading-clmm-routes/remove.ts @@ -9,8 +9,14 @@ import { removeLiquidity as raydiumRemoveLiquidity } from '../../connectors/rayd import { removeLiquidity as uniswapRemoveLiquidity } from '../../connectors/uniswap/clmm-routes/removeLiquidity'; import { RemoveLiquidityResponseType, RemoveLiquidityResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; -import { logger } from '../../services/logger'; -import { CLMM_CONNECTORS, chainNetworkField, connectorField, defaultWallet, parseChainNetwork } from '../common'; +import { + chainNetworkField, + CLMM_CONNECTORS, + connectorField, + defaultWallet, + parseChainNetwork, + rethrowRouteError, +} from '../common'; // Unified schema with connector field const UnifiedRemoveLiquidityRequest = Type.Object({ @@ -100,11 +106,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { throw httpErrors.badRequest(`Unsupported connector: ${connector}`); } } catch (e: any) { - logger.error('Failed to remove liquidity:', e); - if (e.statusCode) { - throw e; - } - throw httpErrors.internalServerError('Failed to remove liquidity'); + rethrowRouteError(e, 'Failed to remove liquidity'); } }, ); From 1559081c1d390ad4f80935b59b8047eb37cf4721 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 18:32:29 -0700 Subject: [PATCH 21/31] fix(trading): connector-config slippage defaults; percentageToRemove 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- .../meteora/clmm-routes/removeLiquidity.ts | 4 +-- src/connectors/meteora/schemas.ts | 2 +- .../orca/clmm-routes/addLiquidity.ts | 13 +++------- .../orca/clmm-routes/executeSwap.ts | 3 ++- .../orca/clmm-routes/openPosition.ts | 3 ++- .../orca/clmm-routes/quotePosition.ts | 3 ++- src/connectors/orca/clmm-routes/quoteSwap.ts | 5 ++-- .../orca/clmm-routes/removeLiquidity.ts | 17 ++++++------ src/connectors/orca/schemas.ts | 4 +-- src/trading/clmm/quote-position.ts | 19 +++++++------- src/trading/common.ts | 18 +++++++++++++ src/trading/swap/execute.ts | 9 ++----- src/trading/swap/quote.ts | 9 ++----- .../trading-amm-routes/add-liquidity.ts | 11 ++------ src/trading/trading-amm-routes/create-pool.ts | 11 +++----- .../trading-amm-routes/execute-swap.ts | 11 ++------ .../trading-amm-routes/quote-liquidity.ts | 19 +++++++------- src/trading/trading-amm-routes/quote-swap.ts | 19 +++++++------- .../trading-amm-routes/remove-liquidity.ts | 11 ++------ src/trading/trading-clmm-routes/add.ts | 11 ++------ src/trading/trading-clmm-routes/open.ts | 11 ++------ src/trading/trading-clmm-routes/remove.ts | 19 +++----------- test/connectors/jupiter/swap.test.js | 26 ++++++++++++------- 23 files changed, 110 insertions(+), 148 deletions(-) diff --git a/src/connectors/meteora/clmm-routes/removeLiquidity.ts b/src/connectors/meteora/clmm-routes/removeLiquidity.ts index 38bee36e56..a661372802 100644 --- a/src/connectors/meteora/clmm-routes/removeLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/removeLiquidity.ts @@ -164,11 +164,11 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { network, walletAddress, positionAddress, liquidityPct } = request.body; + const { network, walletAddress, positionAddress, percentageToRemove } = request.body; const networkToUse = network; - return await removeLiquidity(networkToUse, walletAddress, positionAddress, liquidityPct); + return await removeLiquidity(networkToUse, walletAddress, positionAddress, percentageToRemove); } catch (e) { logger.error(e); if (e.statusCode) { diff --git a/src/connectors/meteora/schemas.ts b/src/connectors/meteora/schemas.ts index 14d0d2ebb0..349681d449 100644 --- a/src/connectors/meteora/schemas.ts +++ b/src/connectors/meteora/schemas.ts @@ -301,7 +301,7 @@ export const MeteoraClmmRemoveLiquidityRequest = Type.Object({ description: 'Position NFT address', examples: [''], }), - liquidityPct: Type.Optional( + percentageToRemove: Type.Optional( Type.Number({ minimum: 0, maximum: 100, diff --git a/src/connectors/orca/clmm-routes/addLiquidity.ts b/src/connectors/orca/clmm-routes/addLiquidity.ts index 24b388b11e..5fb60de375 100644 --- a/src/connectors/orca/clmm-routes/addLiquidity.ts +++ b/src/connectors/orca/clmm-routes/addLiquidity.ts @@ -16,6 +16,7 @@ import { AddLiquidityResponse, AddLiquidityResponseType } from '../../../schemas import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Orca } from '../orca'; +import { OrcaConfig } from '../orca.config'; import { getCurrentTransferFee } from '../orca.position'; import { buildOrcaTransaction, createOrcaAuthority } from '../orca.sdk'; import { OrcaClmmAddLiquidityRequest } from '../schemas'; @@ -26,7 +27,7 @@ export async function addLiquidity( positionAddress: string, baseTokenAmount: number, quoteTokenAmount: number, - slippagePct: number, + slippagePct: number = OrcaConfig.config.slippagePct ?? 1, ): Promise { if ((!baseTokenAmount || baseTokenAmount <= 0) && (!quoteTokenAmount || quoteTokenAmount <= 0)) { throw httpErrors.badRequest('At least one token amount must be provided and greater than 0'); @@ -151,14 +152,8 @@ export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { - walletAddress, - positionAddress, - baseTokenAmount, - quoteTokenAmount, - slippagePct = 1, - network, - } = request.body; + const { walletAddress, positionAddress, baseTokenAmount, quoteTokenAmount, slippagePct, network } = + request.body; return await addLiquidity( network, walletAddress, diff --git a/src/connectors/orca/clmm-routes/executeSwap.ts b/src/connectors/orca/clmm-routes/executeSwap.ts index 970f2c64e1..84b0318b5a 100644 --- a/src/connectors/orca/clmm-routes/executeSwap.ts +++ b/src/connectors/orca/clmm-routes/executeSwap.ts @@ -10,6 +10,7 @@ import { ExecuteSwapResponseType, ExecuteSwapResponse } from '../../../schemas/c import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Orca } from '../orca'; +import { OrcaConfig } from '../orca.config'; import { buildOrcaTransaction, createOrcaAuthority } from '../orca.sdk'; import { OrcaClmmExecuteSwapRequest, OrcaClmmExecuteSwapRequestType } from '../schemas'; @@ -24,7 +25,7 @@ export async function executeSwap( baseTokenIdentifier: string, side: 'BUY' | 'SELL', amount: number, - slippagePct: number = 1, + slippagePct: number = OrcaConfig.config.slippagePct ?? 1, ): Promise { const solana = await Solana.getInstance(network); const orca = await Orca.getInstance(network); diff --git a/src/connectors/orca/clmm-routes/openPosition.ts b/src/connectors/orca/clmm-routes/openPosition.ts index a55ad0aa4c..b0877b9ecb 100644 --- a/src/connectors/orca/clmm-routes/openPosition.ts +++ b/src/connectors/orca/clmm-routes/openPosition.ts @@ -27,6 +27,7 @@ import { OpenPositionResponse, OpenPositionResponseType } from '../../../schemas import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Orca } from '../orca'; +import { OrcaConfig } from '../orca.config'; import { getCurrentTransferFee } from '../orca.position'; import { buildOrcaTransaction, createOrcaAuthority, replaceOrcaInstructionAccounts } from '../orca.sdk'; import { extractInnerTransferAmounts } from '../orca.utils'; @@ -61,7 +62,7 @@ export async function openPosition( throw httpErrors.badRequest('Calculated tick indices are invalid (lower >= upper)'); } - const slippageBps = Math.round((slippagePct || 1) * 100); + const slippageBps = Math.round((slippagePct ?? OrcaConfig.config.slippagePct ?? 1) * 100); const baseAmount = BigInt(Math.floor((baseTokenAmount || 0) * 10 ** mintA.data.decimals)); const quoteAmount = BigInt(Math.floor((quoteTokenAmount || 0) * 10 ** mintB.data.decimals)); const shouldAddLiquidity = baseAmount > 0n || quoteAmount > 0n; diff --git a/src/connectors/orca/clmm-routes/quotePosition.ts b/src/connectors/orca/clmm-routes/quotePosition.ts index 98a94e45c4..b6ac7987e3 100644 --- a/src/connectors/orca/clmm-routes/quotePosition.ts +++ b/src/connectors/orca/clmm-routes/quotePosition.ts @@ -5,6 +5,7 @@ import { QuotePositionResponseType, QuotePositionResponse } from '../../../schem import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Orca } from '../orca'; +import { OrcaConfig } from '../orca.config'; import { quotePosition as getQuotePosition } from '../orca.utils'; import { OrcaClmmQuotePositionRequest } from '../schemas'; @@ -15,7 +16,7 @@ export async function quotePosition( poolAddress: string, baseTokenAmount?: number, quoteTokenAmount?: number, - slippagePct: number = 1, + slippagePct: number = OrcaConfig.config.slippagePct ?? 1, ): Promise { const orca = await Orca.getInstance(network); diff --git a/src/connectors/orca/clmm-routes/quoteSwap.ts b/src/connectors/orca/clmm-routes/quoteSwap.ts index e2c200d041..54aa7b9a40 100644 --- a/src/connectors/orca/clmm-routes/quoteSwap.ts +++ b/src/connectors/orca/clmm-routes/quoteSwap.ts @@ -5,6 +5,7 @@ import { QuoteSwapResponseType, QuoteSwapResponse } from '../../../schemas/clmm- import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Orca } from '../orca'; +import { OrcaConfig } from '../orca.config'; import { getOrcaSwapQuote } from '../orca.utils'; import { OrcaClmmQuoteSwapRequest, OrcaClmmQuoteSwapRequestType } from '../schemas'; @@ -15,7 +16,7 @@ export async function getRawSwapQuote( amount: number, side: 'BUY' | 'SELL', poolAddress: string, - slippagePct: number = 1, + slippagePct: number = OrcaConfig.config.slippagePct ?? 1, ) { const solana = await Solana.getInstance(network); const orca = await Orca.getInstance(network); @@ -50,7 +51,7 @@ async function formatSwapQuote( amount: number, side: 'BUY' | 'SELL', poolAddress: string, - slippagePct: number = 1, + slippagePct: number = OrcaConfig.config.slippagePct ?? 1, ): Promise { const quote = await getRawSwapQuote( network, diff --git a/src/connectors/orca/clmm-routes/removeLiquidity.ts b/src/connectors/orca/clmm-routes/removeLiquidity.ts index 623b12e0a4..83dd7e2bdc 100644 --- a/src/connectors/orca/clmm-routes/removeLiquidity.ts +++ b/src/connectors/orca/clmm-routes/removeLiquidity.ts @@ -12,6 +12,7 @@ import { RemoveLiquidityResponse, RemoveLiquidityResponseType } from '../../../s import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Orca } from '../orca'; +import { OrcaConfig } from '../orca.config'; import { buildOrcaTransaction, createOrcaAuthority } from '../orca.sdk'; import { OrcaClmmRemoveLiquidityRequest } from '../schemas'; @@ -19,11 +20,11 @@ export async function removeLiquidity( network: string, walletAddress: string, positionAddress: string, - liquidityPct: number, - slippagePct: number, + percentageToRemove: number, + slippagePct: number = OrcaConfig.config.slippagePct ?? 1, ): Promise { - if (liquidityPct <= 0 || liquidityPct > 100) { - throw httpErrors.badRequest('liquidityPct must be between 0 and 100'); + if (percentageToRemove <= 0 || percentageToRemove > 100) { + throw httpErrors.badRequest('percentageToRemove must be between 0 and 100'); } const solana = await Solana.getInstance(network); @@ -33,7 +34,7 @@ export async function removeLiquidity( const whirlpool = await fetchWhirlpool(orca.solanaKitRpc, position.data.whirlpool); const [mintA, mintB] = await fetchAllMint(orca.solanaKitRpc, [whirlpool.data.tokenMintA, whirlpool.data.tokenMintB]); const liquidityAmount = BigInt( - new Decimal(position.data.liquidity.toString()).mul(liquidityPct).div(100).floor().toFixed(0), + new Decimal(position.data.liquidity.toString()).mul(percentageToRemove).div(100).floor().toFixed(0), ); if (liquidityAmount <= 0n || liquidityAmount > position.data.liquidity) { @@ -51,7 +52,7 @@ export async function removeLiquidity( }, ); logger.info( - `Removing ${liquidityPct}% liquidity, estimated: ` + + `Removing ${percentageToRemove}% liquidity, estimated: ` + `${(Number(result.quote.tokenEstA) / 10 ** mintA.data.decimals).toFixed(6)} tokenA, ` + `${(Number(result.quote.tokenEstB) / 10 ** mintB.data.decimals).toFixed(6)} tokenB`, ); @@ -99,8 +100,8 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { }, async (request) => { try { - const { walletAddress, positionAddress, liquidityPct = 100, slippagePct = 1, network } = request.body; - return await removeLiquidity(network, walletAddress, positionAddress, liquidityPct, slippagePct); + const { walletAddress, positionAddress, percentageToRemove = 100, slippagePct, network } = request.body; + return await removeLiquidity(network, walletAddress, positionAddress, percentageToRemove, slippagePct); } catch (error) { logger.error(error); if (error.statusCode) throw error; diff --git a/src/connectors/orca/schemas.ts b/src/connectors/orca/schemas.ts index e6c69946ed..1ae92d1b91 100644 --- a/src/connectors/orca/schemas.ts +++ b/src/connectors/orca/schemas.ts @@ -343,7 +343,7 @@ export const OrcaClmmRemoveLiquidityRequest = Type.Object({ description: 'Position NFT address', examples: [''], }), - liquidityPct: Type.Optional( + percentageToRemove: Type.Optional( Type.Number({ minimum: 0, maximum: 100, @@ -357,7 +357,7 @@ export const OrcaClmmRemoveLiquidityRequest = Type.Object({ minimum: 0, maximum: 100, description: 'Maximum acceptable slippage percentage', - default: 1, + default: OrcaConfig.config.slippagePct ?? 1, examples: [1], }), ), diff --git a/src/trading/clmm/quote-position.ts b/src/trading/clmm/quote-position.ts index 4cd2ecfa01..7e66ffdd1d 100644 --- a/src/trading/clmm/quote-position.ts +++ b/src/trading/clmm/quote-position.ts @@ -10,7 +10,14 @@ import { quotePosition as uniswapQuotePosition } from '../../connectors/uniswap/ import { QuotePositionResponseType, QuotePositionResponse } from '../../schemas/clmm-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; -import { chainNetworkField, CLMM_CONNECTORS, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; +import { + chainNetworkField, + CLMM_CONNECTORS, + connectorField, + parseChainNetwork, + rethrowRouteError, + slippagePctField, +} from '../common'; // Constants for examples (using Meteora CLMM values) const BASE_TOKEN_AMOUNT = 0.01; @@ -49,15 +56,7 @@ const UnifiedQuotePositionRequestSchema = Type.Object({ examples: [QUOTE_TOKEN_AMOUNT], }), ), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage', - default: 1, - examples: [1], - }), - ), + slippagePct: slippagePctField(), }); type UnifiedQuotePositionRequest = Static; diff --git a/src/trading/common.ts b/src/trading/common.ts index 75f82a9665..18ecb2ea72 100644 --- a/src/trading/common.ts +++ b/src/trading/common.ts @@ -23,6 +23,24 @@ export const chainNetworkField = () => examples: ['solana-mainnet-beta'], }); +/** + * Optional slippage override shared by the unified trading routes. Deliberately + * has NO schema default: Fastify injects schema defaults before the handler + * runs, so a default here would shadow the connector-level defaults. When + * omitted, each connector applies its own configured slippagePct (falling back + * to 1 where the config has none). + */ +export const slippagePctField = (description?: string) => + Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: + description ?? "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct.", + examples: [1], + }), + ); + /** * Standard catch handler for the unified trading routes: errors that already * carry an HTTP status code (connector badRequest/notFound, chain errors) pass diff --git a/src/trading/swap/execute.ts b/src/trading/swap/execute.ts index 833dad8b1f..bd803b378c 100644 --- a/src/trading/swap/execute.ts +++ b/src/trading/swap/execute.ts @@ -28,7 +28,7 @@ import { ChainExecuteSwapResponseSchema } from '../../schemas/chain-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; import { PoolService } from '../../services/pool-service'; -import { chainNetworkField, defaultWallet, parseChainNetwork, rethrowRouteError } from '../common'; +import { chainNetworkField, defaultWallet, parseChainNetwork, rethrowRouteError, slippagePctField } from '../common'; /** * Unified swap execute request schema @@ -64,12 +64,7 @@ const UnifiedExecuteSwapRequestSchema = Type.Object({ enum: ['BUY', 'SELL'], default: 'SELL', }), - slippagePct: Type.Optional( - Type.Number({ - description: 'Slippage tolerance percentage (optional)', - default: 1, - }), - ), + slippagePct: slippagePctField(), approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: diff --git a/src/trading/swap/quote.ts b/src/trading/swap/quote.ts index 7a3cfefd68..ab26f50427 100644 --- a/src/trading/swap/quote.ts +++ b/src/trading/swap/quote.ts @@ -28,7 +28,7 @@ import { ChainQuoteSwapResponseSchema } from '../../schemas/chain-schema'; import { httpErrors } from '../../services/error-handler'; import { logger } from '../../services/logger'; import { PoolService } from '../../services/pool-service'; -import { chainNetworkField, parseChainNetwork, rethrowRouteError } from '../common'; +import { chainNetworkField, parseChainNetwork, rethrowRouteError, slippagePctField } from '../common'; /** * Unified swap quote request schema @@ -60,12 +60,7 @@ const UnifiedQuoteSwapRequestSchema = Type.Object({ enum: ['BUY', 'SELL'], default: 'SELL', }), - slippagePct: Type.Optional( - Type.Number({ - description: 'Slippage tolerance percentage (optional)', - default: 1, - }), - ), + slippagePct: slippagePctField(), approximateIfNoExactOut: Type.Optional( Type.Boolean({ description: diff --git a/src/trading/trading-amm-routes/add-liquidity.ts b/src/trading/trading-amm-routes/add-liquidity.ts index 8895ee5f83..523e8b0b53 100644 --- a/src/trading/trading-amm-routes/add-liquidity.ts +++ b/src/trading/trading-amm-routes/add-liquidity.ts @@ -14,6 +14,7 @@ import { defaultWallet, parseChainNetwork, rethrowRouteError, + slippagePctField, } from '../common'; const UnifiedAmmAddLiquidityRequest = Type.Object({ @@ -30,15 +31,7 @@ const UnifiedAmmAddLiquidityRequest = Type.Object({ 'position. Ignored by fungible-LP AMMs.', }), ), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage', - default: 1, - examples: [1], - }), - ), + slippagePct: slippagePctField(), }); export const addLiquidityRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-amm-routes/create-pool.ts b/src/trading/trading-amm-routes/create-pool.ts index 4c3d1d5d5c..c8aee96b76 100644 --- a/src/trading/trading-amm-routes/create-pool.ts +++ b/src/trading/trading-amm-routes/create-pool.ts @@ -14,6 +14,7 @@ import { defaultWallet, parseChainNetwork, rethrowRouteError, + slippagePctField, } from '../common'; // Composed from the canonical CreatePoolRequest (schemas/amm-schema.ts): the @@ -43,14 +44,8 @@ const UnifiedCreatePoolRequest = Type.Composite([ description: 'Raydium CPMM fee-config index (optional; defaults to the first available config).', }), ), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Uniswap seeding slippage percentage', - default: 1, - examples: [1], - }), + slippagePct: slippagePctField( + "Uniswap/PancakeSwap seeding slippage percentage. Defaults to the connector's configured slippagePct.", ), }), ]); diff --git a/src/trading/trading-amm-routes/execute-swap.ts b/src/trading/trading-amm-routes/execute-swap.ts index 9d7146b0cf..d0000b9adc 100644 --- a/src/trading/trading-amm-routes/execute-swap.ts +++ b/src/trading/trading-amm-routes/execute-swap.ts @@ -14,6 +14,7 @@ import { defaultWallet, parseChainNetwork, rethrowRouteError, + slippagePctField, } from '../common'; const UnifiedAmmExecuteSwapRequest = Type.Object({ @@ -24,15 +25,7 @@ const UnifiedAmmExecuteSwapRequest = Type.Object({ baseToken: Type.String({ description: 'Base token symbol or address (determines swap direction)' }), amount: Type.Number({ description: 'Amount denominated in the base token' }), side: Type.String({ description: 'Trade direction', enum: ['BUY', 'SELL'] }), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage', - default: 1, - examples: [1], - }), - ), + slippagePct: slippagePctField(), }); export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-amm-routes/quote-liquidity.ts b/src/trading/trading-amm-routes/quote-liquidity.ts index a3cf6080a8..4d4211fd4c 100644 --- a/src/trading/trading-amm-routes/quote-liquidity.ts +++ b/src/trading/trading-amm-routes/quote-liquidity.ts @@ -7,7 +7,14 @@ import { quoteLiquidity as raydiumQuoteLiquidity } from '../../connectors/raydiu import { quoteLiquidity as uniswapQuoteLiquidity } from '../../connectors/uniswap/amm-routes/quoteLiquidity'; import { QuoteLiquidityResponse, QuoteLiquidityResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; +import { + AMM_CONNECTORS, + chainNetworkField, + connectorField, + parseChainNetwork, + rethrowRouteError, + slippagePctField, +} from '../common'; const UnifiedAmmQuoteLiquidityRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -15,15 +22,7 @@ const UnifiedAmmQuoteLiquidityRequest = Type.Object({ poolAddress: Type.String({ description: 'Pool contract address' }), baseTokenAmount: Type.Number({ description: 'Amount of base token to deposit' }), quoteTokenAmount: Type.Number({ description: 'Amount of quote token to deposit' }), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage', - default: 1, - examples: [1], - }), - ), + slippagePct: slippagePctField(), }); export const quoteLiquidityRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-amm-routes/quote-swap.ts b/src/trading/trading-amm-routes/quote-swap.ts index 887c340f2e..bcafd5f38b 100644 --- a/src/trading/trading-amm-routes/quote-swap.ts +++ b/src/trading/trading-amm-routes/quote-swap.ts @@ -7,7 +7,14 @@ import { quoteSwap as raydiumQuoteSwap } from '../../connectors/raydium/amm-rout import { quoteSwap as uniswapQuoteSwap } from '../../connectors/uniswap/amm-routes/quoteSwap'; import { QuoteSwapResponse, QuoteSwapResponseType } from '../../schemas/amm-schema'; import { httpErrors } from '../../services/error-handler'; -import { AMM_CONNECTORS, chainNetworkField, connectorField, parseChainNetwork, rethrowRouteError } from '../common'; +import { + AMM_CONNECTORS, + chainNetworkField, + connectorField, + parseChainNetwork, + rethrowRouteError, + slippagePctField, +} from '../common'; const UnifiedAmmQuoteSwapRequest = Type.Object({ connector: connectorField(AMM_CONNECTORS, 'AMM connector'), @@ -16,15 +23,7 @@ const UnifiedAmmQuoteSwapRequest = Type.Object({ baseToken: Type.String({ description: 'Base token symbol or address (determines swap direction)' }), amount: Type.Number({ description: 'Amount denominated in the base token' }), side: Type.String({ description: 'Trade direction', enum: ['BUY', 'SELL'] }), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage', - default: 1, - examples: [1], - }), - ), + slippagePct: slippagePctField(), }); export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-amm-routes/remove-liquidity.ts b/src/trading/trading-amm-routes/remove-liquidity.ts index 75475db28e..049cf0c77b 100644 --- a/src/trading/trading-amm-routes/remove-liquidity.ts +++ b/src/trading/trading-amm-routes/remove-liquidity.ts @@ -14,6 +14,7 @@ import { defaultWallet, parseChainNetwork, rethrowRouteError, + slippagePctField, } from '../common'; const UnifiedAmmRemoveLiquidityRequest = Type.Object({ @@ -35,15 +36,7 @@ const UnifiedAmmRemoveLiquidityRequest = Type.Object({ default: 100, examples: [100], }), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage', - default: 1, - examples: [1], - }), - ), + slippagePct: slippagePctField(), }); export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { diff --git a/src/trading/trading-clmm-routes/add.ts b/src/trading/trading-clmm-routes/add.ts index 64a5ff6c95..7f4c64bc17 100644 --- a/src/trading/trading-clmm-routes/add.ts +++ b/src/trading/trading-clmm-routes/add.ts @@ -16,6 +16,7 @@ import { defaultWallet, parseChainNetwork, rethrowRouteError, + slippagePctField, } from '../common'; // Constants for examples (using Meteora CLMM values) @@ -46,15 +47,7 @@ const UnifiedAddLiquidityRequest = Type.Object({ examples: [QUOTE_TOKEN_AMOUNT], }), ), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage', - default: 1, - examples: [1], - }), - ), + slippagePct: slippagePctField(), // Meteora-specific parameter (optional, ignored by other connectors). Without it an // add falls back to the connector-config default shape, which can silently differ // from the shape the position was opened with. diff --git a/src/trading/trading-clmm-routes/open.ts b/src/trading/trading-clmm-routes/open.ts index fff88da12e..3c84302ea3 100644 --- a/src/trading/trading-clmm-routes/open.ts +++ b/src/trading/trading-clmm-routes/open.ts @@ -16,6 +16,7 @@ import { defaultWallet, parseChainNetwork, rethrowRouteError, + slippagePctField, } from '../common'; // Constants for examples (using Meteora CLMM values) @@ -57,15 +58,7 @@ const UnifiedOpenPositionRequest = Type.Object({ examples: [QUOTE_TOKEN_AMOUNT], }), ), - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage', - default: 1, - examples: [1], - }), - ), + slippagePct: slippagePctField(), // Meteora-specific parameter (optional, ignored by other connectors) strategyType: Type.Optional( Type.Number({ diff --git a/src/trading/trading-clmm-routes/remove.ts b/src/trading/trading-clmm-routes/remove.ts index d491ca3f77..2f2520cef7 100644 --- a/src/trading/trading-clmm-routes/remove.ts +++ b/src/trading/trading-clmm-routes/remove.ts @@ -16,6 +16,7 @@ import { defaultWallet, parseChainNetwork, rethrowRouteError, + slippagePctField, } from '../common'; // Unified schema with connector field @@ -39,14 +40,8 @@ const UnifiedRemoveLiquidityRequest = Type.Object({ }), // Orca-specific parameter (optional, ignored by other connectors, which manage // slippage internally). - slippagePct: Type.Optional( - Type.Number({ - minimum: 0, - maximum: 100, - description: 'Maximum acceptable slippage percentage. Only applies to the Orca connector.', - default: 1, - examples: [1], - }), + slippagePct: slippagePctField( + "Maximum acceptable slippage percentage. Only applies to the Orca connector; defaults to Orca's configured slippagePct.", ), }); @@ -94,13 +89,7 @@ export const removeLiquidityRoute: FastifyPluginAsync = async (fastify) => { return await pancakeswapSolRemoveLiquidity(network, walletAddress, positionAddress, percentageToRemove); case 'orca': - return await orcaRemoveLiquidity( - network, - walletAddress, - positionAddress, - percentageToRemove, - slippagePct ?? 1, - ); + return await orcaRemoveLiquidity(network, walletAddress, positionAddress, percentageToRemove, slippagePct); default: throw httpErrors.badRequest(`Unsupported connector: ${connector}`); diff --git a/test/connectors/jupiter/swap.test.js b/test/connectors/jupiter/swap.test.js index e6fe869abe..3aa36c249d 100644 --- a/test/connectors/jupiter/swap.test.js +++ b/test/connectors/jupiter/swap.test.js @@ -320,7 +320,7 @@ describe('Jupiter Swap Tests (Solana Mainnet)', () => { ); }); - test('returns successful swap execution with fee parameters', async () => { + test('returns successful swap execution with slippage and BUY-approximation parameters', async () => { // Mock response with status-based format const executeResponse = { signature: '3YHqPTNGFvRjLb6HkBQq8qwsRZ8XNjEjvuehVeNDdz3TxxKnvYBfgMsYCQKNHMpDYzKcUfKdCwzBvkPvDz5aLfYd', @@ -330,7 +330,7 @@ describe('Jupiter Swap Tests (Solana Mainnet)', () => { tokenOut: QUOTE_TOKEN, amountIn: 1.0, amountOut: 16.391234, - fee: 0.002, // Higher fee due to priority + fee: 0.000005, baseTokenBalanceChange: -1.0, quoteTokenBalanceChange: 16.391234, }, @@ -342,7 +342,8 @@ describe('Jupiter Swap Tests (Solana Mainnet)', () => { data: executeResponse, }); - // Make the request with fee parameters + // The request surface is slippagePct + approximateIfNoExactOut only — + // priority fees and routing policy live in connector/network config. const response = await axios.post(`http://localhost:15888/connectors/${CONNECTOR}/execute-swap`, { network: NETWORK, baseToken: BASE_TOKEN, @@ -350,22 +351,29 @@ describe('Jupiter Swap Tests (Solana Mainnet)', () => { side: 'SELL', amount: 1.0, walletAddress: TEST_WALLET, - priorityLevel: 'veryHigh', - maxLamports: 1000000, + slippagePct: 0.5, + approximateIfNoExactOut: false, }); // Validate the response expect(response.status).toBe(200); expect(validateSwapExecution(response.data)).toBe(true); expect(response.data.status).toBe(1); // CONFIRMED - expect(response.data.data.fee).toBe(0.002); // Higher fee + expect(response.data.data.fee).toBe(0.000005); - // Verify axios was called with fee parameters + // Verify axios was called with the supported parameters expect(axios.post).toHaveBeenCalledWith( `http://localhost:15888/connectors/${CONNECTOR}/execute-swap`, expect.objectContaining({ - priorityLevel: 'veryHigh', - maxLamports: 1000000, + slippagePct: 0.5, + approximateIfNoExactOut: false, + }), + ); + expect(axios.post).toHaveBeenCalledWith( + `http://localhost:15888/connectors/${CONNECTOR}/execute-swap`, + expect.not.objectContaining({ + priorityLevel: expect.anything(), + maxLamports: expect.anything(), }), ); }); From 2eaee8b23f3a6546116b4ea62234e026c892b926 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 18:38:13 -0700 Subject: [PATCH 22/31] docs: regenerate openapi.json from current routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- openapi.json | 13505 ++++++++++++++++++++++--------------------------- 1 file changed, 6174 insertions(+), 7331 deletions(-) diff --git a/openapi.json b/openapi.json index dec2716d23..6608f3c96d 100644 --- a/openapi.json +++ b/openapi.json @@ -3,18 +3,10 @@ "info": { "title": "Hummingbot Gateway", "description": "API endpoints for interacting with DEXs and blockchains", - "version": "dev-2.11.0" + "version": "dev-2.17.0" }, "components": { - "parameters": { - "queryExample": { - "in": "query", - "name": "example", - "schema": { - "type": "object" - } - } - }, + "parameters": { "queryExample": { "in": "query", "name": "example", "schema": { "type": "object" } } }, "schemas": {} }, "paths": { @@ -24,22 +16,12 @@ "description": "Get configuration settings. Returns all configurations if no parameters are specified. Use namespace to get a specific config (e.g., server, ethereum-mainnet, solana-mainnet-beta, uniswap).", "parameters": [ { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "server": { - "value": "server" - }, - "ethereum-mainnet": { - "value": "ethereum-mainnet" - }, - "solana-mainnet-beta": { - "value": "solana-mainnet-beta" - }, - "uniswap": { - "value": "uniswap" - } + "server": { "value": "server" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "uniswap": { "value": "uniswap" } }, "in": "query", "name": "namespace", @@ -50,14 +32,7 @@ "responses": { "200": { "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": true - } - } - } + "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } } } } @@ -85,36 +60,18 @@ "value": { "description": "Configuration value", "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "object", - "properties": {} - }, - { - "type": "array", - "items": {} - } + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" }, + { "type": "object", "properties": {} }, + { "type": "array", "items": {} } ] } }, "required": ["namespace", "path", "value"] }, "examples": { - "example1": { - "value": { - "namespace": "solana-mainnet-beta", - "path": "maxFee", - "value": 0.01 - } - }, + "example1": { "value": { "namespace": "solana-mainnet-beta", "path": "maxFee", "value": 0.01 } }, "example2": { "value": { "namespace": "ethereum-mainnet", @@ -123,26 +80,10 @@ } }, "example3": { - "value": { - "namespace": "ethereum-mainnet", - "path": "gasLimitTransaction", - "value": 3000000 - } - }, - "example4": { - "value": { - "namespace": "solana-devnet", - "path": "retryCount", - "value": 5 - } + "value": { "namespace": "ethereum-mainnet", "path": "gasLimitTransaction", "value": 3000000 } }, - "example5": { - "value": { - "namespace": "server", - "path": "port", - "value": 15888 - } - } + "example4": { "value": { "namespace": "solana-devnet", "path": "retryCount", "value": 5 } }, + "example5": { "value": { "namespace": "server", "path": "port", "value": 15888 } } } } }, @@ -155,12 +96,7 @@ "application/json": { "schema": { "type": "object", - "properties": { - "message": { - "description": "Status message", - "type": "string" - } - }, + "properties": { "message": { "description": "Status message", "type": "string" } }, "required": ["message"] } } @@ -186,15 +122,8 @@ "items": { "type": "object", "properties": { - "chain": { - "type": "string" - }, - "networks": { - "type": "array", - "items": { - "type": "string" - } - } + "chain": { "type": "string" }, + "networks": { "type": "array", "items": { "type": "string" } } }, "required": ["chain", "networks"] } @@ -225,24 +154,10 @@ "items": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "trading_types": { - "type": "array", - "items": { - "type": "string" - } - }, - "chain": { - "type": "string" - }, - "networks": { - "type": "array", - "items": { - "type": "string" - } - } + "name": { "type": "string" }, + "trading_types": { "type": "array", "items": { "type": "string" } }, + "chain": { "type": "string" }, + "networks": { "type": "array", "items": { "type": "string" } } }, "required": ["name", "trading_types", "chain", "networks"] } @@ -267,14 +182,7 @@ "application/json": { "schema": { "type": "object", - "properties": { - "namespaces": { - "type": "array", - "items": { - "type": "string" - } - } - }, + "properties": { "namespaces": { "type": "array", "items": { "type": "string" } } }, "required": ["namespaces"] } } @@ -288,15 +196,7 @@ "tags": ["/wallet"], "description": "Get all wallets across different chains", "parameters": [ - { - "schema": { - "default": true, - "type": "boolean" - }, - "in": "query", - "name": "showHardware", - "required": false - } + { "schema": { "default": true, "type": "boolean" }, "in": "query", "name": "showHardware", "required": false } ], "responses": { "200": { @@ -308,11 +208,7 @@ "items": { "type": "object", "properties": { - "chain": { - "description": "Blockchain name", - "type": "string", - "example": "solana" - }, + "chain": { "description": "Blockchain name", "type": "string", "example": "solana" }, "walletAddresses": { "description": "List of regular wallet addresses with private keys", "type": "array", @@ -358,7 +254,7 @@ "privateKey": { "description": "Private key for the wallet", "type": "string", - "example": "\u003Cyour-private-key\u003E" + "example": "" }, "setDefault": { "description": "Set this wallet as the default for the chain", @@ -368,11 +264,7 @@ }, "required": ["chain", "privateKey"] }, - "example": { - "chain": "solana", - "privateKey": "\u003Cyour-private-key\u003E", - "setDefault": true - } + "example": { "chain": "solana", "privateKey": "", "setDefault": true } } }, "required": true @@ -384,12 +276,7 @@ "application/json": { "schema": { "type": "object", - "properties": { - "address": { - "description": "The wallet address that was added", - "type": "string" - } - }, + "properties": { "address": { "description": "The wallet address that was added", "type": "string" } }, "required": ["address"] } } @@ -439,22 +326,10 @@ "schema": { "type": "object", "properties": { - "address": { - "description": "The hardware wallet address that was added", - "type": "string" - }, - "publicKey": { - "description": "Public key of the hardware wallet", - "type": "string" - }, - "derivationPath": { - "description": "BIP32/BIP44 derivation path used", - "type": "string" - }, - "message": { - "description": "Success message", - "type": "string" - } + "address": { "description": "The hardware wallet address that was added", "type": "string" }, + "publicKey": { "description": "Public key of the hardware wallet", "type": "string" }, + "derivationPath": { "description": "BIP32/BIP44 derivation path used", "type": "string" }, + "message": { "description": "Success message", "type": "string" } }, "required": ["address", "publicKey", "derivationPath", "message"] } @@ -480,10 +355,7 @@ "type": "string", "example": "solana" }, - "address": { - "description": "Wallet address to remove", - "type": "string" - } + "address": { "description": "Wallet address to remove", "type": "string" } }, "required": ["chain", "address"] } @@ -499,10 +371,7 @@ "schema": { "type": "object", "properties": { - "message": { - "description": "Success message indicating wallet type removed", - "type": "string" - } + "message": { "description": "Success message indicating wallet type removed", "type": "string" } }, "required": ["message"] } @@ -528,25 +397,16 @@ "type": "string", "example": "solana" }, - "address": { - "description": "Wallet address to set as default", - "type": "string" - } + "address": { "description": "Wallet address to set as default", "type": "string" } }, "required": ["chain", "address"] }, "examples": { "example1": { - "value": { - "chain": "ethereum", - "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2BDf8" - } + "value": { "chain": "ethereum", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2BDf8" } }, "example2": { - "value": { - "chain": "solana", - "address": "7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi" - } + "value": { "chain": "solana", "address": "7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi" } } } } @@ -561,18 +421,9 @@ "schema": { "type": "object", "properties": { - "message": { - "description": "Success message", - "type": "string" - }, - "chain": { - "description": "Chain name", - "type": "string" - }, - "address": { - "description": "Default wallet address", - "type": "string" - } + "message": { "description": "Success message", "type": "string" }, + "chain": { "description": "Chain name", "type": "string" }, + "address": { "description": "Default wallet address", "type": "string" } }, "required": ["message", "chain", "address"] } @@ -588,36 +439,19 @@ "description": "Get a specific token by symbol or address", "parameters": [ { - "schema": { - "type": "string" - }, - "examples": { - "ethereum": { - "value": "ethereum" - }, - "solana": { - "value": "solana" - } - }, + "schema": { "type": "string" }, + "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, "in": "query", "name": "chain", "required": true, "description": "Blockchain network (e.g., ethereum, solana)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "mainnet": { - "value": "mainnet" - }, - "mainnet-beta": { - "value": "mainnet-beta" - }, - "devnet": { - "value": "devnet" - } + "mainnet": { "value": "mainnet" }, + "mainnet-beta": { "value": "mainnet-beta" }, + "devnet": { "value": "devnet" } }, "in": "query", "name": "network", @@ -625,9 +459,7 @@ "description": "Network name (e.g., mainnet, mainnet-beta)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "in": "path", "name": "symbolOrAddress", "required": true, @@ -645,21 +477,13 @@ "token": { "type": "object", "properties": { - "chainId": { - "description": "The chain ID", - "type": "number", - "example": 1 - }, + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, - "symbol": { - "description": "The token symbol", - "type": "string", - "example": "USDC" - }, + "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, "address": { "description": "The token contract address", "type": "string", @@ -671,78 +495,12 @@ "maximum": 255, "type": "number", "example": 6 - }, - "geckoData": { - "type": "object", - "properties": { - "coingeckoCoinId": { - "description": "CoinGecko coin ID if available", - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "imageUrl": { - "description": "Token image URL", - "type": "string" - }, - "priceUsd": { - "description": "Current price in USD", - "type": "string" - }, - "volumeUsd24h": { - "description": "24h trading volume in USD", - "type": "string" - }, - "marketCapUsd": { - "description": "Market capitalization in USD", - "type": "string" - }, - "fdvUsd": { - "description": "Fully diluted valuation in USD", - "type": "string" - }, - "totalSupply": { - "description": "Normalized total supply (human-readable)", - "type": "string" - }, - "topPools": { - "description": "Array of top pool addresses", - "type": "array", - "items": { - "type": "string" - } - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "coingeckoCoinId", - "imageUrl", - "priceUsd", - "volumeUsd24h", - "marketCapUsd", - "fdvUsd", - "totalSupply", - "topPools", - "timestamp" - ] } }, "required": ["name", "symbol", "address", "decimals"] }, - "chain": { - "type": "string" - }, - "network": { - "type": "string" - } + "chain": { "type": "string" }, + "network": { "type": "string" } }, "required": ["token", "chain", "network"] } @@ -758,22 +516,12 @@ "description": "Get token information with market data from GeckoTerminal by address", "parameters": [ { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "solana-mainnet-beta": { - "value": "solana-mainnet-beta" - }, - "ethereum-mainnet": { - "value": "ethereum-mainnet" - }, - "ethereum-base": { - "value": "ethereum-base" - }, - "ethereum-polygon": { - "value": "ethereum-polygon" - } + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } }, "in": "query", "name": "chainNetwork", @@ -781,16 +529,10 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "So11111111111111111111111111111111111111112": { - "value": "So11111111111111111111111111111111111111112" - }, - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { - "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - } + "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } }, "in": "path", "name": "address", @@ -806,21 +548,9 @@ "schema": { "type": "object", "properties": { - "chainId": { - "description": "The chain ID", - "type": "number", - "example": 1 - }, - "name": { - "description": "The full name of the token", - "type": "string", - "example": "USD Coin" - }, - "symbol": { - "description": "The token symbol", - "type": "string", - "example": "USDC" - }, + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, + "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, + "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, "address": { "description": "The token contract address", "type": "string", @@ -832,135 +562,6 @@ "maximum": 255, "type": "number", "example": 6 - }, - "geckoData": { - "type": "object", - "allOf": [ - { - "type": "object", - "properties": { - "coingeckoCoinId": { - "description": "CoinGecko coin ID if available", - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "imageUrl": { - "description": "Token image URL", - "type": "string" - }, - "priceUsd": { - "description": "Current price in USD", - "type": "string" - }, - "volumeUsd24h": { - "description": "24h trading volume in USD", - "type": "string" - }, - "marketCapUsd": { - "description": "Market capitalization in USD", - "type": "string" - }, - "fdvUsd": { - "description": "Fully diluted valuation in USD", - "type": "string" - }, - "totalSupply": { - "description": "Normalized total supply (human-readable)", - "type": "string" - }, - "topPools": { - "description": "Array of top pool addresses", - "type": "array", - "items": { - "type": "string" - } - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "coingeckoCoinId", - "imageUrl", - "priceUsd", - "volumeUsd24h", - "marketCapUsd", - "fdvUsd", - "totalSupply", - "topPools", - "timestamp" - ] - }, - { - "type": "object", - "properties": { - "coingeckoCoinId": { - "description": "CoinGecko coin ID if available", - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "imageUrl": { - "description": "Token image URL", - "type": "string" - }, - "priceUsd": { - "description": "Current price in USD", - "type": "string" - }, - "volumeUsd24h": { - "description": "24h trading volume in USD", - "type": "string" - }, - "marketCapUsd": { - "description": "Market capitalization in USD", - "type": "string" - }, - "fdvUsd": { - "description": "Fully diluted valuation in USD", - "type": "string" - }, - "totalSupply": { - "description": "Normalized total supply (human-readable)", - "type": "string" - }, - "topPools": { - "description": "Array of top pool addresses", - "type": "array", - "items": { - "type": "string" - } - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "coingeckoCoinId", - "imageUrl", - "priceUsd", - "volumeUsd24h", - "marketCapUsd", - "fdvUsd", - "totalSupply", - "topPools", - "timestamp" - ] - } - ] } }, "required": ["name", "symbol", "address", "decimals"] @@ -977,36 +578,19 @@ "description": "List tokens from token lists with optional filtering", "parameters": [ { - "schema": { - "type": "string" - }, - "examples": { - "ethereum": { - "value": "ethereum" - }, - "solana": { - "value": "solana" - } - }, + "schema": { "type": "string" }, + "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, "in": "query", "name": "chain", "required": false, "description": "Blockchain network (e.g., ethereum, solana)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "mainnet": { - "value": "mainnet" - }, - "mainnet-beta": { - "value": "mainnet-beta" - }, - "devnet": { - "value": "devnet" - } + "mainnet": { "value": "mainnet" }, + "mainnet-beta": { "value": "mainnet-beta" }, + "devnet": { "value": "devnet" } }, "in": "query", "name": "network", @@ -1014,17 +598,8 @@ "description": "Network name (e.g., mainnet, mainnet-beta)" }, { - "schema": { - "type": "string" - }, - "examples": { - "USDC": { - "value": "USDC" - }, - "USD": { - "value": "USD" - } - }, + "schema": { "type": "string" }, + "examples": { "USDC": { "value": "USDC" }, "USD": { "value": "USD" } }, "in": "query", "name": "search", "required": false, @@ -1044,21 +619,13 @@ "items": { "type": "object", "properties": { - "chainId": { - "description": "The chain ID", - "type": "number", - "example": 1 - }, + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, - "symbol": { - "description": "The token symbol", - "type": "string", - "example": "USDC" - }, + "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, "address": { "description": "The token contract address", "type": "string", @@ -1070,68 +637,6 @@ "maximum": 255, "type": "number", "example": 6 - }, - "geckoData": { - "type": "object", - "properties": { - "coingeckoCoinId": { - "description": "CoinGecko coin ID if available", - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "imageUrl": { - "description": "Token image URL", - "type": "string" - }, - "priceUsd": { - "description": "Current price in USD", - "type": "string" - }, - "volumeUsd24h": { - "description": "24h trading volume in USD", - "type": "string" - }, - "marketCapUsd": { - "description": "Market capitalization in USD", - "type": "string" - }, - "fdvUsd": { - "description": "Fully diluted valuation in USD", - "type": "string" - }, - "totalSupply": { - "description": "Normalized total supply (human-readable)", - "type": "string" - }, - "topPools": { - "description": "Array of top pool addresses", - "type": "array", - "items": { - "type": "string" - } - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "coingeckoCoinId", - "imageUrl", - "priceUsd", - "volumeUsd24h", - "marketCapUsd", - "fdvUsd", - "totalSupply", - "topPools", - "timestamp" - ] } }, "required": ["name", "symbol", "address", "decimals"] @@ -1167,21 +672,9 @@ "token": { "type": "object", "properties": { - "chainId": { - "description": "The chain ID", - "type": "number", - "example": 1 - }, - "name": { - "description": "The full name of the token", - "type": "string", - "example": "USD Coin" - }, - "symbol": { - "description": "The token symbol", - "type": "string", - "example": "USDC" - }, + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, + "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, + "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, "address": { "description": "The token contract address", "type": "string", @@ -1193,68 +686,6 @@ "maximum": 255, "type": "number", "example": 6 - }, - "geckoData": { - "type": "object", - "properties": { - "coingeckoCoinId": { - "description": "CoinGecko coin ID if available", - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "imageUrl": { - "description": "Token image URL", - "type": "string" - }, - "priceUsd": { - "description": "Current price in USD", - "type": "string" - }, - "volumeUsd24h": { - "description": "24h trading volume in USD", - "type": "string" - }, - "marketCapUsd": { - "description": "Market capitalization in USD", - "type": "string" - }, - "fdvUsd": { - "description": "Fully diluted valuation in USD", - "type": "string" - }, - "totalSupply": { - "description": "Normalized total supply (human-readable)", - "type": "string" - }, - "topPools": { - "description": "Array of top pool addresses", - "type": "array", - "items": { - "type": "string" - } - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "coingeckoCoinId", - "imageUrl", - "priceUsd", - "volumeUsd24h", - "marketCapUsd", - "fdvUsd", - "totalSupply", - "topPools", - "timestamp" - ] } }, "required": ["name", "symbol", "address", "decimals"] @@ -1273,18 +704,8 @@ "application/json": { "schema": { "type": "object", - "properties": { - "message": { - "description": "Success message", - "type": "string" - }, - "requiresRestart": { - "description": "Whether gateway restart is required", - "default": true, - "type": "boolean" - } - }, - "required": ["message", "requiresRestart"] + "properties": { "message": { "description": "Success message", "type": "string" } }, + "required": ["message"] } } } @@ -1298,22 +719,12 @@ "description": "Find token from GeckoTerminal and save it to the token list", "parameters": [ { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "solana-mainnet-beta": { - "value": "solana-mainnet-beta" - }, - "ethereum-mainnet": { - "value": "ethereum-mainnet" - }, - "ethereum-base": { - "value": "ethereum-base" - }, - "ethereum-polygon": { - "value": "ethereum-polygon" - } + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } }, "in": "query", "name": "chainNetwork", @@ -1321,16 +732,10 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "So11111111111111111111111111111111111111112": { - "value": "So11111111111111111111111111111111111111112" - }, - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { - "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" - } + "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } }, "in": "path", "name": "address", @@ -1346,27 +751,17 @@ "schema": { "type": "object", "properties": { - "message": { - "type": "string" - }, + "message": { "type": "string" }, "token": { "type": "object", "properties": { - "chainId": { - "description": "The chain ID", - "type": "number", - "example": 1 - }, + "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, - "symbol": { - "description": "The token symbol", - "type": "string", - "example": "USDC" - }, + "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, "address": { "description": "The token contract address", "type": "string", @@ -1378,68 +773,6 @@ "maximum": 255, "type": "number", "example": 6 - }, - "geckoData": { - "type": "object", - "properties": { - "coingeckoCoinId": { - "description": "CoinGecko coin ID if available", - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] - }, - "imageUrl": { - "description": "Token image URL", - "type": "string" - }, - "priceUsd": { - "description": "Current price in USD", - "type": "string" - }, - "volumeUsd24h": { - "description": "24h trading volume in USD", - "type": "string" - }, - "marketCapUsd": { - "description": "Market capitalization in USD", - "type": "string" - }, - "fdvUsd": { - "description": "Fully diluted valuation in USD", - "type": "string" - }, - "totalSupply": { - "description": "Normalized total supply (human-readable)", - "type": "string" - }, - "topPools": { - "description": "Array of top pool addresses", - "type": "array", - "items": { - "type": "string" - } - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "coingeckoCoinId", - "imageUrl", - "priceUsd", - "volumeUsd24h", - "marketCapUsd", - "fdvUsd", - "totalSupply", - "topPools", - "timestamp" - ] } }, "required": ["name", "symbol", "address", "decimals"] @@ -1459,36 +792,19 @@ "description": "Remove a token from a token list by address", "parameters": [ { - "schema": { - "type": "string" - }, - "examples": { - "ethereum": { - "value": "ethereum" - }, - "solana": { - "value": "solana" - } - }, + "schema": { "type": "string" }, + "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, "in": "query", "name": "chain", "required": true, "description": "Blockchain network (e.g., ethereum, solana)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "mainnet": { - "value": "mainnet" - }, - "mainnet-beta": { - "value": "mainnet-beta" - }, - "devnet": { - "value": "devnet" - } + "mainnet": { "value": "mainnet" }, + "mainnet-beta": { "value": "mainnet-beta" }, + "devnet": { "value": "devnet" } }, "in": "query", "name": "network", @@ -1496,9 +812,7 @@ "description": "Network name (e.g., mainnet, mainnet-beta)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "in": "path", "name": "address", "required": true, @@ -1512,18 +826,8 @@ "application/json": { "schema": { "type": "object", - "properties": { - "message": { - "description": "Success message", - "type": "string" - }, - "requiresRestart": { - "description": "Whether gateway restart is required", - "default": true, - "type": "boolean" - } - }, - "required": ["message", "requiresRestart"] + "properties": { "message": { "description": "Success message", "type": "string" } }, + "required": ["message"] } } } @@ -1537,76 +841,45 @@ "description": "Get a specific pool by trading pair", "parameters": [ { - "schema": { - "type": "string" - }, - "examples": { - "raydium": { - "value": "raydium" - }, - "meteora": { - "value": "meteora" - }, - "uniswap": { - "value": "uniswap" - }, - "orca": { - "value": "orca" - } - }, + "schema": { "type": "string" }, + "examples": { "solana": { "value": "solana" }, "ethereum": { "value": "ethereum" } }, "in": "query", - "name": "connector", + "name": "chain", "required": true, - "description": "Connector (raydium, meteora, uniswap, orca)" + "description": "Blockchain chain (solana, ethereum)" }, { - "schema": { - "default": "mainnet-beta", - "type": "string" - }, - "examples": { - "mainnet-beta": { - "value": "mainnet-beta" - }, - "mainnet": { - "value": "mainnet" - } - }, + "schema": { "default": "mainnet-beta", "type": "string" }, + "examples": { "mainnet-beta": { "value": "mainnet-beta" }, "mainnet": { "value": "mainnet" } }, "in": "query", "name": "network", "required": true, "description": "Network name (mainnet, mainnet-beta, etc)" }, { - "schema": { - "enum": ["amm", "clmm"], - "type": "string" - }, - "examples": { - "amm": { - "value": "amm" - }, - "clmm": { - "value": "clmm" - } - }, + "schema": { "enum": ["amm", "clmm"], "type": "string" }, + "examples": { "amm": { "value": "amm" }, "clmm": { "value": "clmm" } }, "in": "query", "name": "type", "required": true, "description": "Pool type" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "SOL-USDC": { - "value": "SOL-USDC" - }, - "ETH-USDC": { - "value": "ETH-USDC" - } + "raydium": { "value": "raydium" }, + "meteora": { "value": "meteora" }, + "uniswap": { "value": "uniswap" }, + "orca": { "value": "orca" } }, + "in": "query", + "name": "connector", + "required": false, + "description": "Optional: filter by connector (raydium, meteora, uniswap, orca)" + }, + { + "schema": { "type": "string" }, + "examples": { "SOL-USDC": { "value": "SOL-USDC" }, "ETH-USDC": { "value": "ETH-USDC" } }, "in": "path", "name": "tradingPair", "required": true, @@ -1621,35 +894,27 @@ "schema": { "type": "object", "properties": { + "connector": { + "description": "Connector name (raydium, uniswap, orca, etc)", + "type": "string", + "example": "raydium" + }, "type": { "description": "Pool type", "enum": ["clmm", "amm"], "type": "string", "example": "clmm" }, - "network": { - "type": "string" - }, - "baseSymbol": { - "type": "string" - }, - "quoteSymbol": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "feePct": { - "type": "number" - }, - "address": { - "type": "string" - } + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" } }, "required": [ + "connector", "type", "network", "baseSymbol", @@ -1666,16 +931,7 @@ "404": { "description": "Default Response", "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - } - } + "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } } } } @@ -1687,22 +943,12 @@ "description": "Get detailed pool information by address from GeckoTerminal", "parameters": [ { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "solana-mainnet-beta": { - "value": "solana-mainnet-beta" - }, - "ethereum-mainnet": { - "value": "ethereum-mainnet" - }, - "ethereum-base": { - "value": "ethereum-base" - }, - "ethereum-polygon": { - "value": "ethereum-polygon" - } + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } }, "in": "query", "name": "chainNetwork", @@ -1710,16 +956,12 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2": { "value": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" }, - "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { - "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" - } + "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" } }, "in": "path", "name": "address", @@ -1735,81 +977,27 @@ "schema": { "type": "object", "properties": { + "connector": { + "description": "Connector name (raydium, uniswap, orca, etc)", + "type": "string", + "example": "raydium" + }, "type": { "description": "Pool type", "enum": ["clmm", "amm"], "type": "string", "example": "clmm" }, - "network": { - "type": "string" - }, - "baseSymbol": { - "type": "string" - }, - "quoteSymbol": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "feePct": { - "type": "number" - }, - "address": { - "type": "string" - }, - "geckoData": { - "type": "object", - "properties": { - "volumeUsd24h": { - "description": "24-hour trading volume in USD", - "type": "string" - }, - "liquidityUsd": { - "description": "Total liquidity in USD", - "type": "string" - }, - "priceNative": { - "description": "Base token price in quote token", - "type": "string" - }, - "priceUsd": { - "description": "Base token price in USD", - "type": "string" - }, - "buys24h": { - "description": "Number of buy transactions in 24h", - "type": "number" - }, - "sells24h": { - "description": "Number of sell transactions in 24h", - "type": "number" - }, - "apr": { - "description": "Annual percentage rate", - "type": "number" - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "volumeUsd24h", - "liquidityUsd", - "priceNative", - "priceUsd", - "buys24h", - "sells24h", - "timestamp" - ] - } + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" } }, "required": [ + "connector", "type", "network", "baseSymbol", @@ -1832,22 +1020,12 @@ "description": "Find pools for a token pair from GeckoTerminal", "parameters": [ { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "solana-mainnet-beta": { - "value": "solana-mainnet-beta" - }, - "ethereum-mainnet": { - "value": "ethereum-mainnet" - }, - "ethereum-base": { - "value": "ethereum-base" - }, - "ethereum-polygon": { - "value": "ethereum-polygon" - } + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } }, "in": "query", "name": "chainNetwork", @@ -1855,28 +1033,14 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "raydium": { - "value": "raydium" - }, - "meteora": { - "value": "meteora" - }, - "uniswap": { - "value": "uniswap" - }, - "pancakeswap": { - "value": "pancakeswap" - }, - "pancakeswap-sol": { - "value": "pancakeswap-sol" - }, - "orca": { - "value": "orca" - } + "raydium": { "value": "raydium" }, + "meteora": { "value": "meteora" }, + "uniswap": { "value": "uniswap" }, + "pancakeswap": { "value": "pancakeswap" }, + "pancakeswap-sol": { "value": "pancakeswap-sol" }, + "orca": { "value": "orca" } }, "in": "query", "name": "connector", @@ -1884,38 +1048,19 @@ "description": "Filter by connector name (e.g., raydium, meteora, uniswap, pancakeswap, pancakeswap-sol)" }, { - "schema": { - "enum": ["clmm", "amm"], - "default": "clmm", - "type": "string" - }, - "examples": { - "clmm": { - "value": "clmm" - }, - "amm": { - "value": "amm" - } - }, + "schema": { "enum": ["clmm", "amm"], "default": "clmm", "type": "string" }, + "examples": { "clmm": { "value": "clmm" }, "amm": { "value": "amm" } }, "in": "query", "name": "type", "required": false, "description": "Filter by pool type: clmm (v3-style concentrated liquidity) or amm (v2-style)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "SOL": { - "value": "SOL" - }, - "So11111111111111111111111111111111111111112": { - "value": "So11111111111111111111111111111111111111112" - }, - "USDC": { - "value": "USDC" - } + "SOL": { "value": "SOL" }, + "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, + "USDC": { "value": "USDC" } }, "in": "query", "name": "tokenA", @@ -1923,19 +1068,13 @@ "description": "First token symbol or contract address (optional - for filtering by token pair)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "USDC": { - "value": "USDC" - }, + "USDC": { "value": "USDC" }, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": { "value": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }, - "SOL": { - "value": "SOL" - } + "SOL": { "value": "SOL" } }, "in": "query", "name": "tokenB", @@ -1943,12 +1082,7 @@ "description": "Second token symbol or contract address (optional - for filtering by token pair)" }, { - "schema": { - "minimum": 1, - "maximum": 10, - "default": 10, - "type": "number" - }, + "schema": { "minimum": 1, "maximum": 10, "default": 10, "type": "number" }, "in": "query", "name": "pages", "required": false, @@ -1965,81 +1099,27 @@ "items": { "type": "object", "properties": { + "connector": { + "description": "Connector name (raydium, uniswap, orca, etc)", + "type": "string", + "example": "raydium" + }, "type": { "description": "Pool type", "enum": ["clmm", "amm"], "type": "string", "example": "clmm" }, - "network": { - "type": "string" - }, - "baseSymbol": { - "type": "string" - }, - "quoteSymbol": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "feePct": { - "type": "number" - }, - "address": { - "type": "string" - }, - "geckoData": { - "type": "object", - "properties": { - "volumeUsd24h": { - "description": "24-hour trading volume in USD", - "type": "string" - }, - "liquidityUsd": { - "description": "Total liquidity in USD", - "type": "string" - }, - "priceNative": { - "description": "Base token price in quote token", - "type": "string" - }, - "priceUsd": { - "description": "Base token price in USD", - "type": "string" - }, - "buys24h": { - "description": "Number of buy transactions in 24h", - "type": "number" - }, - "sells24h": { - "description": "Number of sell transactions in 24h", - "type": "number" - }, - "apr": { - "description": "Annual percentage rate", - "type": "number" - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "volumeUsd24h", - "liquidityUsd", - "priceNative", - "priceUsd", - "buys24h", - "sells24h", - "timestamp" - ] - } + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" } }, "required": [ + "connector", "type", "network", "baseSymbol", @@ -2060,73 +1140,52 @@ "/pools/": { "get": { "tags": ["/pools"], - "description": "List all pools for a connector, optionally filtered by network, type, or search term", + "description": "List all pools for a chain/network, optionally filtered by connector, type, or search term", "parameters": [ { - "schema": { - "type": "string" - }, - "examples": { - "raydium": { - "value": "raydium" - }, - "meteora": { - "value": "meteora" - }, - "uniswap": { - "value": "uniswap" - }, - "orca": { - "value": "orca" - } - }, + "schema": { "type": "string" }, + "examples": { "solana": { "value": "solana" }, "ethereum": { "value": "ethereum" } }, "in": "query", - "name": "connector", + "name": "chain", "required": true, - "description": "Connector (raydium, meteora, uniswap, orca)" + "description": "Blockchain chain (solana, ethereum)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "mainnet": { - "value": "mainnet" - }, - "mainnet-beta": { - "value": "mainnet-beta" - }, - "base": { - "value": "base" - } + "mainnet-beta": { "value": "mainnet-beta" }, + "mainnet": { "value": "mainnet" }, + "base": { "value": "base" }, + "arbitrum": { "value": "arbitrum" } }, "in": "query", "name": "network", - "required": false, - "description": "Optional: filter by network (mainnet, mainnet-beta, etc)" + "required": true, + "description": "Network name (mainnet-beta, mainnet, base, etc)" }, { - "schema": { - "enum": ["clmm", "amm"], - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "clmm": { - "value": "clmm" - }, - "amm": { - "value": "amm" - } + "raydium": { "value": "raydium" }, + "meteora": { "value": "meteora" }, + "uniswap": { "value": "uniswap" }, + "orca": { "value": "orca" } }, "in": "query", + "name": "connector", + "required": false, + "description": "Optional: filter by connector (raydium, meteora, uniswap, orca)" + }, + { + "schema": { "enum": ["clmm", "amm"], "type": "string" }, + "examples": { "clmm": { "value": "clmm" }, "amm": { "value": "amm" } }, + "in": "query", "name": "type", "required": false, "description": "Optional: filter by pool type" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "in": "query", "name": "search", "required": false, @@ -2143,35 +1202,27 @@ "items": { "type": "object", "properties": { + "connector": { + "description": "Connector name (raydium, uniswap, orca, etc)", + "type": "string", + "example": "raydium" + }, "type": { "description": "Pool type", "enum": ["clmm", "amm"], "type": "string", "example": "clmm" }, - "network": { - "type": "string" - }, - "baseSymbol": { - "type": "string" - }, - "quoteSymbol": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "feePct": { - "type": "number" - }, - "address": { - "type": "string" - } + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" } }, "required": [ + "connector", "type", "network", "baseSymbol", @@ -2197,34 +1248,31 @@ "schema": { "type": "object", "properties": { + "chain": { + "description": "Blockchain chain (solana, ethereum)", + "type": "string", + "example": "solana" + }, "connector": { "description": "Connector (raydium, meteora, uniswap, orca)", "type": "string", "example": "raydium" }, - "type": { - "description": "Pool type", - "enum": ["clmm", "amm"], - "type": "string", - "example": "clmm" - }, + "type": { "description": "Pool type", "enum": ["clmm", "amm"], "type": "string", "example": "clmm" }, "network": { "description": "Network name (mainnet, mainnet-beta, etc)", "default": "mainnet-beta", "type": "string", "example": "mainnet-beta" }, - "address": { - "description": "Pool contract address", - "type": "string" - }, + "address": { "description": "Pool contract address", "type": "string" }, "baseSymbol": { - "description": "Base token symbol", + "description": "Base token symbol (optional - fetched automatically if not provided)", "type": "string", "example": "SOL" }, "quoteSymbol": { - "description": "Quote token symbol", + "description": "Quote token symbol (optional - fetched automatically if not provided)", "type": "string", "example": "USDC" }, @@ -2247,12 +1295,11 @@ } }, "required": [ + "chain", "connector", "type", "network", "address", - "baseSymbol", - "quoteSymbol", "baseTokenAddress", "quoteTokenAddress" ] @@ -2268,11 +1315,7 @@ "application/json": { "schema": { "type": "object", - "properties": { - "message": { - "type": "string" - } - }, + "properties": { "message": { "type": "string" } }, "required": ["message"] } } @@ -2281,16 +1324,7 @@ "400": { "description": "Default Response", "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - } - } + "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } } } } @@ -2299,25 +1333,15 @@ "/pools/save/{address}": { "post": { "tags": ["/pools"], - "description": "Find pool from GeckoTerminal and save it to the pool list", + "description": "Find pool from GeckoTerminal and save it to the pool list. Auto-adds missing tokens.", "parameters": [ { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { - "solana-mainnet-beta": { - "value": "solana-mainnet-beta" - }, - "ethereum-mainnet": { - "value": "ethereum-mainnet" - }, - "ethereum-base": { - "value": "ethereum-base" - }, - "ethereum-polygon": { - "value": "ethereum-polygon" - } + "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, + "ethereum-mainnet": { "value": "ethereum-mainnet" }, + "ethereum-base": { "value": "ethereum-base" }, + "ethereum-polygon": { "value": "ethereum-polygon" } }, "in": "query", "name": "chainNetwork", @@ -2325,16 +1349,12 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "examples": { "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2": { "value": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" }, - "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { - "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" - } + "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" } }, "in": "path", "name": "address", @@ -2350,87 +1370,31 @@ "schema": { "type": "object", "properties": { - "message": { - "type": "string" - }, + "message": { "type": "string" }, "pool": { "type": "object", "properties": { + "connector": { + "description": "Connector name (raydium, uniswap, orca, etc)", + "type": "string", + "example": "raydium" + }, "type": { "description": "Pool type", "enum": ["clmm", "amm"], "type": "string", "example": "clmm" }, - "network": { - "type": "string" - }, - "baseSymbol": { - "type": "string" - }, - "quoteSymbol": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "feePct": { - "type": "number" - }, - "address": { - "type": "string" - }, - "geckoData": { - "type": "object", - "properties": { - "volumeUsd24h": { - "description": "24-hour trading volume in USD", - "type": "string" - }, - "liquidityUsd": { - "description": "Total liquidity in USD", - "type": "string" - }, - "priceNative": { - "description": "Base token price in quote token", - "type": "string" - }, - "priceUsd": { - "description": "Base token price in USD", - "type": "string" - }, - "buys24h": { - "description": "Number of buy transactions in 24h", - "type": "number" - }, - "sells24h": { - "description": "Number of sell transactions in 24h", - "type": "number" - }, - "apr": { - "description": "Annual percentage rate", - "type": "number" - }, - "timestamp": { - "description": "Unix timestamp (ms) when data was fetched", - "type": "number" - } - }, - "required": [ - "volumeUsd24h", - "liquidityUsd", - "priceNative", - "priceUsd", - "buys24h", - "sells24h", - "timestamp" - ] - } + "network": { "type": "string" }, + "baseSymbol": { "type": "string" }, + "quoteSymbol": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "address": { "type": "string" } }, "required": [ + "connector", "type", "network", "baseSymbol", @@ -2440,7 +1404,8 @@ "feePct", "address" ] - } + }, + "tokensAdded": { "type": "array", "items": { "type": "string" } } }, "required": ["message", "pool"] } @@ -2456,66 +1421,23 @@ "description": "Remove a pool by address", "parameters": [ { - "schema": { - "type": "string" - }, - "examples": { - "raydium": { - "value": "raydium" - }, - "meteora": { - "value": "meteora" - }, - "uniswap": { - "value": "uniswap" - }, - "orca": { - "value": "orca" - } - }, + "schema": { "type": "string" }, + "examples": { "solana": { "value": "solana" }, "ethereum": { "value": "ethereum" } }, "in": "query", - "name": "connector", + "name": "chain", "required": true, - "description": "Connector (raydium, meteora, uniswap, orca)" + "description": "Blockchain chain (solana, ethereum)" }, { - "schema": { - "type": "string" - }, - "examples": { - "mainnet": { - "value": "mainnet" - }, - "mainnet-beta": { - "value": "mainnet-beta" - } - }, + "schema": { "type": "string" }, + "examples": { "mainnet": { "value": "mainnet" }, "mainnet-beta": { "value": "mainnet-beta" } }, "in": "query", "name": "network", "required": true, "description": "Network name (mainnet, mainnet-beta, etc)" }, { - "schema": { - "type": "string" - }, - "examples": { - "amm": { - "value": "amm" - }, - "clmm": { - "value": "clmm" - } - }, - "in": "query", - "name": "type", - "required": true, - "description": "Pool type" - }, - { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "in": "path", "name": "address", "required": true, @@ -2529,11 +1451,7 @@ "application/json": { "schema": { "type": "object", - "properties": { - "message": { - "type": "string" - } - }, + "properties": { "message": { "type": "string" } }, "required": ["message"] } } @@ -2542,16 +1460,7 @@ "404": { "description": "Default Response", "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - } - } + "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } } } } @@ -2563,75 +1472,63 @@ "description": "Get a swap quote for any supported chain", "parameters": [ { - "schema": { - "default": "solana-mainnet-beta", - "type": "string" - }, + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", "required": true, - "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet, ethereum-polygon)" + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "default": "jupiter/router", - "type": "string" - }, + "schema": { "type": "string" }, + "example": "jupiter/router", "in": "query", "name": "connector", "required": false, "description": "Connector to use in format: connector/type (e.g., jupiter/router, raydium/amm, uniswap/clmm). If not provided, uses network's configured swapProvider" }, { - "schema": { - "default": "SOL", - "type": "string" - }, + "schema": { "default": "SOL", "type": "string" }, "in": "query", "name": "baseToken", "required": true, "description": "Symbol or address of the base token" }, { - "schema": { - "default": "USDC", - "type": "string" - }, + "schema": { "default": "USDC", "type": "string" }, "in": "query", "name": "quoteToken", "required": true, "description": "Symbol or address of the quote token" }, { - "schema": { - "default": 1, - "type": "number" - }, + "schema": { "default": 1, "type": "number" }, "in": "query", "name": "amount", "required": true, "description": "Amount to swap" }, { - "schema": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "in": "query", "name": "side", "required": true, "description": "Side of the swap" }, { - "schema": { - "default": 1, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "example": 1, "in": "query", "name": "slippagePct", "required": false, - "description": "Slippage tolerance percentage (optional)" + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct." + }, + { + "schema": { "default": true, "type": "boolean" }, + "in": "query", + "name": "approximateIfNoExactOut", + "required": false, + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing. Solana routers only." } ], "responses": { @@ -2642,50 +1539,20 @@ "schema": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token being swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token being swapped to", - "type": "string" - }, - "amountIn": { - "description": "Amount of tokenIn to be swapped", - "type": "number" - }, - "amountOut": { - "description": "Expected amount of tokenOut to receive", - "type": "number" - }, - "price": { - "description": "Exchange rate between tokenIn and tokenOut", - "type": "number" - }, - "priceImpactPct": { - "description": "Estimated price impact percentage (0-100)", - "type": "number" - }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { - "description": "Maximum amount of tokenIn that will be spent", - "type": "number" - }, - "poolAddress": { - "description": "Pool address for AMM/CLMM swaps", - "type": "string" - }, - "routePath": { - "description": "Route path for router-based swaps", - "type": "string" - }, - "slippagePct": { - "description": "Slippage tolerance percentage", - "type": "number" - } + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "poolAddress": { "description": "Pool address for AMM/CLMM swaps", "type": "string" }, + "routePath": { "description": "Route path for router-based swaps", "type": "string" }, + "slippagePct": { "description": "Slippage tolerance percentage", "type": "number" } }, "required": [ "tokenIn", @@ -2716,18 +1583,19 @@ "properties": { "walletAddress": { "description": "Wallet address to execute swap from", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "chainNetwork": { - "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet, ethereum-polygon)", + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", "default": "solana-mainnet-beta", - "type": "string" + "type": "string", + "example": "solana-mainnet-beta" }, "connector": { "description": "Connector to use in format: connector/type (e.g., jupiter/router, raydium/amm, uniswap/clmm). If not provided, uses network's configured swapProvider", - "default": "jupiter/router", - "type": "string" + "type": "string", + "example": "jupiter/router" }, "baseToken": { "description": "Symbol or address of the base token", @@ -2739,11 +1607,7 @@ "default": "USDC", "type": "string" }, - "amount": { - "description": "Amount to swap", - "default": 1, - "type": "number" - }, + "amount": { "description": "Amount to swap", "default": 1, "type": "number" }, "side": { "description": "Side of the swap", "enum": ["BUY", "SELL"], @@ -2751,9 +1615,16 @@ "type": "string" }, "slippagePct": { - "description": "Slippage tolerance percentage (optional)", - "default": 1, - "type": "number" + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct.", + "type": "number", + "example": 1 + }, + "approximateIfNoExactOut": { + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing. Solana routers only.", + "default": true, + "type": "boolean" } }, "required": ["walletAddress", "chainNetwork", "baseToken", "quoteToken", "amount", "side"] @@ -2770,10 +1641,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -2781,26 +1649,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -2836,7 +1689,7 @@ "parameters": [ { "schema": { - "enum": ["raydium", "meteora", "pancakeswap-sol", "uniswap", "pancakeswap", "orca"], + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string" }, @@ -2844,13 +1697,10 @@ "in": "query", "name": "connector", "required": true, - "description": "CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap, orca)" + "description": "CLMM connector" }, { - "schema": { - "default": "solana-mainnet-beta", - "type": "string" - }, + "schema": { "default": "solana-mainnet-beta", "type": "string" }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -2858,14 +1708,19 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", "in": "query", "name": "poolAddress", "required": true, "description": "Pool contract address" + }, + { + "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "in": "query", + "name": "binCount", + "required": false, + "description": "If > 0, include a `bins` array of per-tick liquidity around the active tick. Supported by every connector except Meteora, which always returns its bins and ignores this. Default 0 = skip the bin fetch." } ], "responses": { @@ -2876,32 +1731,28 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"], + "title": "BinLiquidity" + } } }, "required": [ @@ -2928,7 +1779,7 @@ "parameters": [ { "schema": { - "enum": ["raydium", "meteora", "pancakeswap-sol", "uniswap", "pancakeswap", "orca"], + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string" }, @@ -2936,13 +1787,10 @@ "in": "query", "name": "connector", "required": true, - "description": "CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap, orca)" + "description": "CLMM connector" }, { - "schema": { - "default": "solana-mainnet-beta", - "type": "string" - }, + "schema": { "default": "solana-mainnet-beta", "type": "string" }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -2950,10 +1798,8 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "type": "string" - }, - "example": "\u003Csample-position-address\u003E", + "schema": { "type": "string" }, + "example": "", "in": "query", "name": "positionAddress", "required": true, @@ -2968,51 +1814,19 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -3043,7 +1857,7 @@ "parameters": [ { "schema": { - "enum": ["raydium", "meteora", "pancakeswap-sol", "uniswap", "pancakeswap", "orca"], + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string" }, @@ -3051,13 +1865,10 @@ "in": "query", "name": "connector", "required": true, - "description": "CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap, orca)" + "description": "CLMM connector" }, { - "schema": { - "default": "solana-mainnet-beta", - "type": "string" - }, + "schema": { "default": "solana-mainnet-beta", "type": "string" }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -3065,14 +1876,11 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", - "type": "string" - }, + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "in": "query", "name": "walletAddress", - "required": false, - "description": "Wallet address (optional, uses default wallet if not provided)" + "required": true, + "description": "Wallet address" } ], "responses": { @@ -3085,51 +1893,19 @@ "items": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -3162,7 +1938,7 @@ "parameters": [ { "schema": { - "enum": ["raydium", "meteora", "pancakeswap-sol", "uniswap", "pancakeswap", "orca"], + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string" }, @@ -3170,13 +1946,10 @@ "in": "query", "name": "connector", "required": true, - "description": "CLMM connector (raydium, meteora, pancakeswap-sol, uniswap, pancakeswap, orca)" + "description": "CLMM connector" }, { - "schema": { - "default": "solana-mainnet-beta", - "type": "string" - }, + "schema": { "default": "solana-mainnet-beta", "type": "string" }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -3184,9 +1957,7 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 150, "in": "query", "name": "lowerPrice", @@ -3194,9 +1965,7 @@ "description": "Lower price bound for the position" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 250, "in": "query", "name": "upperPrice", @@ -3204,9 +1973,7 @@ "description": "Upper price bound for the position" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", "in": "query", "name": "poolAddress", @@ -3214,9 +1981,7 @@ "description": "Pool contract address" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -3224,9 +1989,7 @@ "description": "Amount of base token to deposit" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -3234,17 +1997,12 @@ "description": "Amount of quote token to deposit" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 1, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false, - "description": "Maximum acceptable slippage percentage" + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct." } ], "responses": { @@ -3255,21 +2013,11 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - }, + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, "liquidity": {} }, "required": [ @@ -3297,7 +2045,8 @@ "type": "object", "properties": { "connector": { - "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)", + "description": "CLMM connector", + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string", "example": "meteora" @@ -3310,7 +2059,7 @@ }, "walletAddress": { "description": "Wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "lowerPrice": { @@ -3341,10 +2090,14 @@ "slippagePct": { "minimum": 0, "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 1, + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct.", "type": "number", "example": 1 + }, + "strategyType": { + "description": "Strategy type for Meteora positions (0=Spot, 1=Curve). Only applies to Meteora connector.", + "type": "number", + "example": 0 } }, "required": ["connector", "chainNetwork", "walletAddress", "lowerPrice", "upperPrice", "poolAddress"] @@ -3361,31 +2114,16 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionAddress": { - "type": "string" - }, - "positionRent": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": [ "fee", @@ -3415,7 +2153,8 @@ "type": "object", "properties": { "connector": { - "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)", + "description": "CLMM connector", + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string", "example": "meteora" @@ -3428,41 +2167,38 @@ }, "walletAddress": { "description": "Wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "positionAddress": { "description": "Position address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" }, "baseTokenAmount": { - "description": "Amount of base token to deposit", + "description": "Amount of base token to deposit (omit for single-sided quote deposit)", "type": "number", "example": 0.01 }, "quoteTokenAmount": { - "description": "Amount of quote token to deposit", + "description": "Amount of quote token to deposit (omit for single-sided base deposit)", "type": "number", "example": 2 }, "slippagePct": { "minimum": 0, "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 1, + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct.", "type": "number", "example": 1 + }, + "strategyType": { + "description": "Strategy type for Meteora positions (0=Spot, 1=Curve). Only applies to Meteora connector.", + "type": "number", + "example": 0 } }, - "required": [ - "connector", - "chainNetwork", - "walletAddress", - "positionAddress", - "baseTokenAmount", - "quoteTokenAmount" - ] + "required": ["connector", "chainNetwork", "walletAddress", "positionAddress"] } } }, @@ -3476,25 +2212,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -3518,7 +2243,8 @@ "type": "object", "properties": { "connector": { - "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)", + "description": "CLMM connector", + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string", "example": "meteora" @@ -3531,13 +2257,13 @@ }, "walletAddress": { "description": "Wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "positionAddress": { "description": "Position address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" }, "percentageToRemove": { "minimum": 0, @@ -3546,6 +2272,13 @@ "default": 100, "type": "number", "example": 100 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage. Only applies to the Orca connector; defaults to Orca's configured slippagePct.", + "type": "number", + "example": 1 } }, "required": ["connector", "chainNetwork", "walletAddress", "positionAddress", "percentageToRemove"] @@ -3562,25 +2295,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -3604,7 +2326,8 @@ "type": "object", "properties": { "connector": { - "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)", + "description": "CLMM connector", + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string", "example": "meteora" @@ -3617,13 +2340,13 @@ }, "walletAddress": { "description": "Wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "positionAddress": { "description": "Position address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" } }, "required": ["connector", "chainNetwork", "walletAddress", "positionAddress"] @@ -3640,25 +2363,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -3682,7 +2394,8 @@ "type": "object", "properties": { "connector": { - "description": "Connector name (uniswap, pancakeswap, raydium, meteora, pancakeswap-sol, orca)", + "description": "CLMM connector", + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], "default": "meteora", "type": "string", "example": "meteora" @@ -3695,13 +2408,13 @@ }, "walletAddress": { "description": "Wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "positionAddress": { "description": "Position address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" } }, "required": ["connector", "chainNetwork", "walletAddress", "positionAddress"] @@ -3718,34 +2431,17 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionRentRefunded": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": [ "fee", @@ -3765,23 +2461,59 @@ } } }, - "/chains/solana/status": { - "get": { - "tags": ["/chain/solana"], - "description": "Get Solana network status", - "parameters": [ - { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "in": "query", - "name": "network", - "required": false, - "description": "The Solana network to use" - } - ], + "/trading/clmm/create-pool": { + "post": { + "tags": ["/trading/clmm"], + "description": "Create and initialize a new CLMM pool across supported connectors (Meteora DLMM, Raydium CLMM, PancakeSwap Solana CLMM, Orca Whirlpool, Uniswap V3, PancakeSwap V3)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "CLMM connector", + "enum": ["meteora", "raydium", "pancakeswap-sol", "orca", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address (pool creator + payer)", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "baseToken": { "type": "string" }, + "quoteToken": { "type": "string" }, + "initialPrice": { + "description": "Initial pool price as quote per base. If omitted, the current market price is fetched from the unified swap router so the pool opens on-market.", + "type": "number" + }, + "binStep": { + "description": "Bin/tick granularity: Meteora DLMM bin step (bps); Orca Whirlpool tick spacing.", + "type": "number" + }, + "feeBps": { + "description": "Base fee in basis points: Meteora DLMM base fee; Uniswap/PancakeSwap V3 fee tier (1, 5, 30 or 100 bps; PancakeSwap also 25).", + "type": "number" + }, + "ammConfigIndex": { + "description": "Fee-config index for the Raydium CLMM family: Raydium API config list index; pancakeswap-sol amm_config PDA index. Default 0.", + "type": "number" + } + }, + "required": ["connector", "chainNetwork", "walletAddress", "baseToken", "quoteToken"] + } + } + }, + "required": true + }, "responses": { "200": { "description": "Default Response", @@ -3790,37 +2522,16 @@ "schema": { "type": "object", "properties": { - "chain": { - "type": "string" - }, - "network": { - "type": "string" - }, - "rpcUrl": { - "type": "string" + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was initialized at (quote per base)", + "type": "number" }, - "rpcProvider": { - "type": "string" - }, - "currentBlockNumber": { - "type": "number" - }, - "nativeCurrency": { - "type": "string" - }, - "swapProvider": { - "type": "string" - } + "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } }, - "required": [ - "chain", - "network", - "rpcUrl", - "rpcProvider", - "currentBlockNumber", - "nativeCurrency", - "swapProvider" - ] + "required": ["signature", "status", "poolAddress"] } } } @@ -3828,21 +2539,37 @@ } } }, - "/chains/solana/estimate-gas": { + "/trading/amm/pool-info": { "get": { - "tags": ["/chain/solana"], - "description": "Estimate gas prices for Solana transactions", + "tags": ["/trading/amm"], + "description": "Get AMM pool information from any supported connector", "parameters": [ { "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", "type": "string" }, + "example": "meteora", "in": "query", - "name": "network", - "required": false, - "description": "The Solana network to use" + "name": "connector", + "required": true, + "description": "AMM connector" + }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "type": "string" }, + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Pool contract address" } ], "responses": { @@ -3853,35 +2580,23 @@ "schema": { "type": "object", "properties": { - "feePerComputeUnit": { - "type": "number" - }, - "denomination": { - "type": "string" - }, - "computeUnits": { - "type": "number" - }, - "feeAsset": { - "type": "string" - }, - "fee": { - "type": "number" - }, - "timestamp": { - "type": "number" - }, - "gasType": { - "type": "string" - }, - "maxFeePerGas": { - "type": "number" - }, - "maxPriorityFeePerGas": { - "type": "number" - } + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } }, - "required": ["feePerComputeUnit", "denomination", "computeUnits", "feeAsset", "fee", "timestamp"] + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount" + ] } } } @@ -3889,64 +2604,93 @@ } } }, - "/chains/solana/balances": { - "post": { - "tags": ["/chain/solana"], - "description": "Get token balances for a Solana address. Only returns tokens in the network's token list. If no tokens specified or empty array provided, returns non-zero balances for tokens from the token list that are found in the wallet (includes SOL even if zero). If specific tokens are requested, returns those exact tokens with their balances, including zeros.", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { - "description": "The Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "address": { - "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", - "type": "string" - }, - "tokens": { - "description": "A list of token symbols (SOL, USDC, BONK) from the network's token list. Only tokens in the token list will be returned. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of SOL).", - "type": "array", - "items": { - "type": "string" - }, - "example": ["SOL", "USDC", "BONK"] - } - } - } - } + "/trading/amm/position-info": { + "get": { + "tags": ["/trading/amm"], + "description": "Get a wallet's aggregated AMM liquidity in a pool from any supported connector", + "parameters": [ + { + "schema": { + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string" + }, + "example": "meteora", + "in": "query", + "name": "connector", + "required": true, + "description": "AMM connector" + }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "type": "string" }, + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Pool contract address" + }, + { + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": true, + "description": "Wallet address" } - }, + ], "responses": { "200": { - "description": "Token balances for the specified address (only tokens in token list)", + "description": "Default Response", "content": { "application/json": { "schema": { "type": "object", "properties": { - "balances": { - "type": "object", - "additionalProperties": { - "type": "number" + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "positionAddress": { + "description": "Address of the individual position (NFT position account)", + "type": "string" + }, + "lpTokenAmount": { + "description": "Liquidity held by this position (LP units)", + "type": "number" + }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"], + "title": "PositionDetail" } } }, - "required": ["balances"], - "description": "Token balances for the specified address (only tokens in token list)" - }, - "example": { - "balances": { - "SOL": 1.5, - "USDC": 100, - "BONK": 50000 - } + "required": [ + "poolAddress", + "walletAddress", + "baseTokenAddress", + "quoteTokenAddress", + "lpTokenAmount", + "baseTokenAmount", + "quoteTokenAmount", + "price" + ] } } } @@ -3954,107 +2698,187 @@ } } }, - "/chains/solana/poll": { - "post": { - "tags": ["/chain/solana"], - "description": "Poll for the status of a Solana transaction", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "network": { - "description": "The Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "signature": { - "description": "Transaction signature to poll", - "type": "string", - "example": "55ukR6VCt1sQFMC8Nyeo51R1SMaTzUC7jikmkEJ2jjkQNdqBxXHraH7vaoaNmf8rX4Y55EXAj8XXoyzvvsrQqWZa" - }, - "tokens": { - "description": "Tokens to track balance changes for", - "type": "array", - "items": { - "type": "string" - }, - "example": ["SOL", "USDC", "BONK"] - }, - "walletAddress": { - "description": "Wallet address to track balance changes for", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", - "type": "string" - } - }, - "required": ["signature"] - } - } + "/trading/amm/positions-owned": { + "get": { + "tags": ["/trading/amm"], + "description": "List all AMM positions a wallet owns across pools. Supported only for non-fungible-LP AMMs (meteora DAMM v2). Fungible-LP AMMs (raydium, uniswap, pancakeswap) have no enumerable positions — use position-info with a specific pool address instead.", + "parameters": [ + { + "schema": { + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string" + }, + "example": "meteora", + "in": "query", + "name": "connector", + "required": true, + "description": "AMM connector (only non-fungible-LP AMMs supported: meteora)" }, - "required": true - }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": true, + "description": "Wallet address to list positions for" + } + ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "currentBlock": { - "type": "number" - }, - "signature": { - "type": "string" - }, - "txBlock": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - }, - "txStatus": { - "type": "number" - }, - "fee": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - }, - "tokenBalanceChanges": { - "description": "Dictionary of token balance changes keyed by token input value (symbol or address)", - "type": "object", - "additionalProperties": { - "type": "number" - } - }, - "txData": { - "anyOf": [ - { + "type": "array", + "items": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" }, + "positions": { + "type": "array", + "items": { "type": "object", - "additionalProperties": {} - }, - { - "type": "null" + "properties": { + "positionAddress": { + "description": "Address of the individual position (NFT position account)", + "type": "string" + }, + "lpTokenAmount": { + "description": "Liquidity held by this position (LP units)", + "type": "number" + }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] } - ] + } }, - "error": { - "type": "string" - } + "required": [ + "poolAddress", + "walletAddress", + "baseTokenAddress", + "quoteTokenAddress", + "lpTokenAmount", + "baseTokenAmount", + "quoteTokenAmount", + "price" + ] + } + } + } + } + } + } + } + }, + "/trading/amm/quote-swap": { + "get": { + "tags": ["/trading/amm"], + "description": "Get a swap quote against a specific AMM pool from any supported connector", + "parameters": [ + { + "schema": { + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string" + }, + "example": "meteora", + "in": "query", + "name": "connector", + "required": true, + "description": "AMM connector" + }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "type": "string" }, + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Pool contract address" + }, + { + "schema": { "type": "string" }, + "in": "query", + "name": "baseToken", + "required": true, + "description": "Base token symbol or address (determines swap direction)" + }, + { + "schema": { "type": "number" }, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount denominated in the base token" + }, + { + "schema": { "enum": ["BUY", "SELL"], "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction" + }, + { + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct." + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, - "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "txData"] + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] } } } @@ -4062,34 +2886,133 @@ } } }, - "/chains/solana/wrap": { + "/trading/amm/quote-liquidity": { + "get": { + "tags": ["/trading/amm"], + "description": "Quote amounts for adding liquidity to an AMM pool from any supported connector", + "parameters": [ + { + "schema": { + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string" + }, + "example": "meteora", + "in": "query", + "name": "connector", + "required": true, + "description": "AMM connector" + }, + { + "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "example": "solana-mainnet-beta", + "in": "query", + "name": "chainNetwork", + "required": true, + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" + }, + { + "schema": { "type": "string" }, + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Pool contract address" + }, + { + "schema": { "type": "number" }, + "in": "query", + "name": "baseTokenAmount", + "required": true, + "description": "Amount of base token to deposit" + }, + { + "schema": { "type": "number" }, + "in": "query", + "name": "quoteTokenAmount", + "required": true, + "description": "Amount of quote token to deposit" + }, + { + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct." + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" } + }, + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] + } + } + } + } + } + } + }, + "/trading/amm/execute-swap": { "post": { - "tags": ["/chain/solana"], - "description": "Wrap SOL to WSOL (Wrapped SOL)", + "tags": ["/trading/amm"], + "description": "Execute a swap against a specific AMM pool from any supported connector", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { - "description": "The Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "connector": { + "description": "AMM connector", + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "address": { - "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "poolAddress": { "description": "Pool contract address", "type": "string" }, + "baseToken": { + "description": "Base token symbol or address (determines swap direction)", "type": "string" }, - "amount": { - "description": "The amount of SOL to wrap (in SOL, not lamports)", - "type": "string", - "example": "1.0" + "amount": { "description": "Amount denominated in the base token", "type": "number" }, + "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], "type": "string" }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct.", + "type": "number", + "example": 1 } }, - "required": ["amount"] + "required": ["connector", "chainNetwork", "walletAddress", "poolAddress", "baseToken", "amount", "side"] } } }, @@ -4103,33 +3026,28 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "string" - }, - "amount": { - "type": "string" - }, - "wrappedAddress": { - "type": "string" - }, - "nativeToken": { - "type": "string" - }, - "wrappedToken": { - "type": "string" - } + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } }, - "required": ["fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] } }, "required": ["signature", "status"] @@ -4140,36 +3058,61 @@ } } }, - "/chains/solana/unwrap": { + "/trading/amm/add-liquidity": { "post": { - "tags": ["/chain/solana"], - "description": "Unwrap WSOL to SOL. Note: This closes the entire WSOL account, returning all WSOL as SOL.", + "tags": ["/trading/amm"], + "description": "Add liquidity to an AMM pool from any supported connector", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { - "network": { - "description": "The Solana network to use", - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "connector": { + "description": "AMM connector", + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "address": { - "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "poolAddress": { "description": "Pool contract address", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "positionAddress": { + "description": "meteora only (DAMM v2 positions are NFTs): add to this specific position. Omit to open a new position. Ignored by fungible-LP AMMs.", "type": "string" }, - "amount": { - "description": "The amount of WSOL to unwrap (in SOL, not lamports). If not provided, unwraps all WSOL.", - "type": "string", - "example": "1.0" + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct.", + "type": "number", + "example": 1 } - } + }, + "required": [ + "connector", + "chainNetwork", + "walletAddress", + "poolAddress", + "baseTokenAmount", + "quoteTokenAmount" + ] } } - } + }, + "required": true }, "responses": { "200": { @@ -4179,36 +3122,202 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/trading/amm/remove-liquidity": { + "post": { + "tags": ["/trading/amm"], + "description": "Remove liquidity from an AMM pool from any supported connector", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "AMM connector", + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "poolAddress": { "description": "Pool contract address", "type": "string" }, + "positionAddress": { + "description": "Required for meteora (DAMM v2 positions are NFTs): the specific position to remove from. List positions with position-info or positions-owned. Ignored by fungible-LP AMMs.", + "type": "string" + }, + "percentageToRemove": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of liquidity to remove", + "default": 100, + "type": "number", + "example": 100 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct.", + "type": "number", + "example": 1 + } + }, + "required": ["connector", "chainNetwork", "walletAddress", "poolAddress", "percentageToRemove"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/trading/amm/create-pool": { + "post": { + "tags": ["/trading/amm"], + "description": "Create and seed a new AMM pool across supported connectors (Meteora DAMM v2, Raydium CPMM, Uniswap V2, PancakeSwap V2)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "AMM connector", + "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], + "default": "meteora", + "type": "string", + "example": "meteora" + }, + "chainNetwork": { + "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", + "default": "solana-mainnet-beta", + "type": "string", + "example": "solana-mainnet-beta" + }, + "walletAddress": { + "description": "Wallet address (pool creator + payer)", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", + "type": "string" + }, + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string" + }, + "baseTokenAmount": { "description": "Amount of base token to seed the pool with", "type": "number" }, + "quoteTokenAmount": { + "description": "Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. If omitted (and no initialPrice), the price is fetched from the market.", + "type": "number" + }, + "initialPrice": { + "description": "Initial price as quote per base. Overrides quoteTokenAmount. If both are omitted, the current market price is fetched from the unified swap router so the pool opens on-market.", + "type": "number" + }, + "configAddress": { + "description": "Meteora DAMM v2 config account address (required for the meteora connector — configs are permissionless accounts with no index derivation, so the address must be explicit).", + "type": "string" + }, + "ammConfigIndex": { + "description": "Raydium CPMM fee-config index (optional; defaults to the first available config).", + "type": "number" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Uniswap/PancakeSwap seeding slippage percentage. Defaults to the connector's configured slippagePct.", + "type": "number", + "example": 1 + } + }, + "required": ["connector", "chainNetwork", "walletAddress", "baseToken", "quoteToken", "baseTokenAmount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was seeded at (quote per base)", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "string" - }, - "amount": { - "type": "string" - }, - "wrappedAddress": { - "type": "string" - }, - "nativeToken": { - "type": "string" - }, - "wrappedToken": { - "type": "string" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, - "required": ["fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } }, - "required": ["signature", "status"] + "required": ["signature", "status", "poolAddress"] } } } @@ -4216,21 +3325,17 @@ } } }, - "/chains/ethereum/status": { + "/chains/solana/status": { "get": { - "tags": ["/chain/ethereum"], - "description": "Get Ethereum chain status", + "tags": ["/chain/solana"], + "description": "Get Solana network status", "parameters": [ { - "schema": { - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon", "sepolia"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, - "description": "The Ethereum network to use" + "description": "The Solana network to use" } ], "responses": { @@ -4241,27 +3346,13 @@ "schema": { "type": "object", "properties": { - "chain": { - "type": "string" - }, - "network": { - "type": "string" - }, - "rpcUrl": { - "type": "string" - }, - "rpcProvider": { - "type": "string" - }, - "currentBlockNumber": { - "type": "number" - }, - "nativeCurrency": { - "type": "string" - }, - "swapProvider": { - "type": "string" - } + "chain": { "type": "string" }, + "network": { "type": "string" }, + "rpcUrl": { "type": "string" }, + "rpcProvider": { "type": "string" }, + "currentBlockNumber": { "type": "number" }, + "nativeCurrency": { "type": "string" }, + "swapProvider": { "type": "string" } }, "required": [ "chain", @@ -4279,21 +3370,17 @@ } } }, - "/chains/ethereum/estimate-gas": { + "/chains/solana/estimate-gas": { "get": { - "tags": ["/chain/ethereum"], - "description": "Estimate gas prices for Ethereum transactions", + "tags": ["/chain/solana"], + "description": "Estimate priority fees for Solana transactions. Optionally pass addresses (program IDs, pools) for Helius-specific fee estimation.", "parameters": [ { - "schema": { - "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon", "sepolia"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, - "description": "The Ethereum network to use" + "description": "The Solana network to use" } ], "responses": { @@ -4304,33 +3391,17 @@ "schema": { "type": "object", "properties": { - "feePerComputeUnit": { - "type": "number" - }, - "denomination": { - "type": "string" - }, - "computeUnits": { - "type": "number" - }, - "feeAsset": { - "type": "string" - }, - "fee": { - "type": "number" - }, - "timestamp": { - "type": "number" - }, - "gasType": { - "type": "string" - }, - "maxFeePerGas": { - "type": "number" - }, - "maxPriorityFeePerGas": { - "type": "number" - } + "feePerComputeUnit": { "type": "number" }, + "denomination": { "type": "string" }, + "computeUnits": { "type": "number" }, + "feeAsset": { "type": "string" }, + "fee": { "type": "number" }, + "timestamp": { "type": "number" }, + "gasType": { "type": "string" }, + "maxFeePerGas": { "type": "number" }, + "maxPriorityFeePerGas": { "type": "number" }, + "priorityFeeLevel": { "type": "string" }, + "priorityFeePerCUEstimate": { "type": "number" } }, "required": ["feePerComputeUnit", "denomination", "computeUnits", "feeAsset", "fee", "timestamp"] } @@ -4340,10 +3411,10 @@ } } }, - "/chains/ethereum/balances": { + "/chains/solana/balances": { "post": { - "tags": ["/chain/ethereum"], - "description": "Get Ethereum balances. If no tokens specified or empty array provided, returns native token (ETH) and only non-zero balances for tokens from the token list. If specific tokens are requested, returns those exact tokens with their balances, including zeros.", + "tags": ["/chain/solana"], + "description": "Get token balances for a Solana address. Only returns tokens in the network's token list. If no tokens specified or empty array provided, returns non-zero balances for tokens from the token list that are found in the wallet (includes SOL even if zero). If specific tokens are requested, returns those exact tokens with their balances, including zeros.", "requestBody": { "content": { "application/json": { @@ -4351,33 +3422,21 @@ "type": "object", "properties": { "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], + "description": "The Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], "type": "string" }, "address": { - "description": "Ethereum wallet address", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "description": "Solana wallet address", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "tokens": { - "description": "A list of token symbols (ETH, USDC, WETH) or token addresses. Both formats are accepted and will be automatically detected. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of ETH).", + "description": "A list of token symbols (SOL, USDC, BONK) from the network's token list. Only tokens in the token list will be returned. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of SOL).", "type": "array", - "items": { - "type": "string" - }, - "example": ["ETH", "USDC", "WETH"] + "items": { "type": "string" }, + "example": ["SOL", "USDC", "BONK"] } } } @@ -4386,31 +3445,26 @@ }, "responses": { "200": { - "description": "Default Response", + "description": "Token balances for the specified address (only tokens in token list)", "content": { "application/json": { "schema": { "type": "object", - "properties": { - "balances": { - "type": "object", - "additionalProperties": { - "type": "number" - } - } - }, - "required": ["balances"] - } + "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, + "required": ["balances"], + "description": "Token balances for the specified address (only tokens in token list)" + }, + "example": { "balances": { "SOL": 1.5, "USDC": 100, "BONK": 50000 } } } } } } } }, - "/chains/ethereum/poll": { + "/chains/solana/poll": { "post": { - "tags": ["/chain/ethereum"], - "description": "Poll Ethereum transaction status", + "tags": ["/chain/solana"], + "description": "Poll for the status of a Solana transaction", "requestBody": { "content": { "application/json": { @@ -4418,25 +3472,15 @@ "type": "object", "properties": { "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], + "description": "The Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], "type": "string" }, "signature": { - "description": "Transaction hash to poll", + "description": "Transaction signature to poll", "type": "string", - "example": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + "example": "55ukR6VCt1sQFMC8Nyeo51R1SMaTzUC7jikmkEJ2jjkQNdqBxXHraH7vaoaNmf8rX4Y55EXAj8XXoyzvvsrQqWZa" } }, "required": ["signature"] @@ -4453,58 +3497,23 @@ "schema": { "type": "object", "properties": { - "currentBlock": { - "type": "number" - }, - "signature": { - "type": "string" - }, - "txBlock": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - }, + "currentBlock": { "type": "number" }, + "signature": { "type": "string" }, + "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, "txStatus": { + "description": "Transaction status: 1 = confirmed, 0 = pending, -1 = failed, -2 = not found (unknown to the chain: never received or dropped; on Solana this is terminal once the transaction blockhash expires)", "type": "number" }, - "fee": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "null" - } - ] - }, - "tokenBalanceChanges": { - "description": "Dictionary of token balance changes keyed by token input value (symbol or address)", - "type": "object", - "additionalProperties": { - "type": "number" - } - }, - "txData": { + "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "error": { "anyOf": [ - { - "type": "object", - "additionalProperties": {} - }, - { - "type": "null" - } + { "description": "Error info if failed: \"TYPE (code): message\"", "type": "string" }, + { "type": "null" } ] }, - "error": { - "type": "string" - } + "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] } }, - "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "txData"] + "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "error", "txData"] } } } @@ -4512,10 +3521,10 @@ } } }, - "/chains/ethereum/allowances": { + "/chains/solana/wrap": { "post": { - "tags": ["/chain/ethereum"], - "description": "Get token allowances", + "tags": ["/chain/solana"], + "description": "Wrap SOL to WSOL (Wrapped SOL)", "requestBody": { "content": { "application/json": { @@ -4523,41 +3532,23 @@ "type": "object", "properties": { "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], + "description": "The Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], "type": "string" }, "address": { - "description": "Ethereum wallet address", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "description": "Solana wallet address", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "spender": { - "description": "Connector name (e.g., uniswap/clmm, uniswap/amm, 0x/router) or contract address", + "amount": { + "description": "The amount of SOL to wrap (in SOL, not lamports)", "type": "string", - "example": "uniswap/router" - }, - "tokens": { - "description": "Array of token symbols or addresses", - "type": "array", - "items": { - "type": "string" - }, - "example": ["USDC", "WETH"] + "example": "1.0" } }, - "required": ["spender", "tokens"] + "required": ["amount"] } } }, @@ -4571,17 +3562,21 @@ "schema": { "type": "object", "properties": { - "spender": { - "type": "string" - }, - "approvals": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { "type": "object", - "additionalProperties": { - "type": "string" - } + "properties": { + "fee": { "type": "string" }, + "amount": { "type": "string" }, + "wrappedAddress": { "type": "string" }, + "nativeToken": { "type": "string" }, + "wrappedToken": { "type": "string" } + }, + "required": ["fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] } }, - "required": ["spender", "approvals"] + "required": ["signature", "status"] } } } @@ -4589,10 +3584,10 @@ } } }, - "/chains/ethereum/approve": { + "/chains/solana/unwrap": { "post": { - "tags": ["/chain/ethereum"], - "description": "Approve token spending", + "tags": ["/chain/solana"], + "description": "Unwrap WSOL to SOL. Note: This closes the entire WSOL account, returning all WSOL as SOL.", "requestBody": { "content": { "application/json": { @@ -4600,47 +3595,25 @@ "type": "object", "properties": { "network": { - "description": "The Ethereum network to use", - "default": "mainnet", - "enum": [ - "arbitrum", - "avalanche", - "base", - "bsc", - "celo", - "mainnet", - "optimism", - "polygon", - "sepolia" - ], + "description": "The Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], "type": "string" }, "address": { - "description": "Ethereum wallet address", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "description": "Solana wallet address", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "spender": { - "description": "Connector name (e.g., uniswap/clmm, uniswap/amm, 0x/router) contract address", - "type": "string", - "example": "uniswap/router" - }, - "token": { - "description": "Token symbol or address", - "type": "string", - "example": "USDC" - }, "amount": { - "description": "The amount to approve. If not provided, defaults to maximum amount (unlimited approval).", - "default": "", - "type": "string" + "description": "The amount of WSOL to unwrap (in SOL, not lamports). If not provided, unwraps all WSOL.", + "type": "string", + "example": "1.0" } - }, - "required": ["spender", "token"] + } } } - }, - "required": true + } }, "responses": { "200": { @@ -4650,33 +3623,18 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "tokenAddress": { - "type": "string" - }, - "spender": { - "type": "string" - }, - "amount": { - "type": "string" - }, - "nonce": { - "type": "number" - }, - "fee": { - "type": "string" - } + "fee": { "type": "string" }, + "amount": { "type": "string" }, + "wrappedAddress": { "type": "string" }, + "nativeToken": { "type": "string" }, + "wrappedToken": { "type": "string" } }, - "required": ["tokenAddress", "spender", "amount", "nonce", "fee"] + "required": ["fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] } }, "required": ["signature", "status"] @@ -4687,10 +3645,130 @@ } } }, - "/chains/ethereum/wrap": { + "/chains/ethereum/status": { + "get": { + "tags": ["/chain/ethereum"], + "description": "Get Ethereum chain status", + "parameters": [ + { + "schema": { + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain-testnet", + "robinhoodchain", + "sepolia", + "unichain" + ], + "type": "string" + }, + "in": "query", + "name": "network", + "required": false, + "description": "The Ethereum network to use" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "chain": { "type": "string" }, + "network": { "type": "string" }, + "rpcUrl": { "type": "string" }, + "rpcProvider": { "type": "string" }, + "currentBlockNumber": { "type": "number" }, + "nativeCurrency": { "type": "string" }, + "swapProvider": { "type": "string" } + }, + "required": [ + "chain", + "network", + "rpcUrl", + "rpcProvider", + "currentBlockNumber", + "nativeCurrency", + "swapProvider" + ] + } + } + } + } + } + } + }, + "/chains/ethereum/estimate-gas": { + "get": { + "tags": ["/chain/ethereum"], + "description": "Estimate gas prices for Ethereum transactions", + "parameters": [ + { + "schema": { + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain-testnet", + "robinhoodchain", + "sepolia", + "unichain" + ], + "type": "string" + }, + "in": "query", + "name": "network", + "required": false, + "description": "The Ethereum network to use" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feePerComputeUnit": { "type": "number" }, + "denomination": { "type": "string" }, + "computeUnits": { "type": "number" }, + "feeAsset": { "type": "string" }, + "fee": { "type": "number" }, + "timestamp": { "type": "number" }, + "gasType": { "type": "string" }, + "maxFeePerGas": { "type": "number" }, + "maxPriorityFeePerGas": { "type": "number" }, + "priorityFeeLevel": { "type": "string" }, + "priorityFeePerCUEstimate": { "type": "number" } + }, + "required": ["feePerComputeUnit", "denomination", "computeUnits", "feeAsset", "fee", "timestamp"] + } + } + } + } + } + } + }, + "/chains/ethereum/balances": { "post": { "tags": ["/chain/ethereum"], - "description": "Wrap native token to wrapped token (e.g., ETH to WETH, BNB to WBNB)", + "description": "Get Ethereum balances. If no tokens specified or empty array provided, returns native token (ETH) and only non-zero balances for tokens from the token list. If specific tokens are requested, returns those exact tokens with their balances, including zeros.", "requestBody": { "content": { "application/json": { @@ -4709,26 +3787,28 @@ "mainnet", "optimism", "polygon", - "sepolia" + "robinhoodchain-testnet", + "robinhoodchain", + "sepolia", + "unichain" ], "type": "string" }, "address": { "description": "Ethereum wallet address", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "amount": { - "description": "The amount of native token to wrap (e.g., ETH, BNB, AVAX)", - "type": "string", - "example": "0.01" + "tokens": { + "description": "A list of token symbols (ETH, USDC, WETH) or token addresses. Both formats are accepted and will be automatically detected. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of ETH).", + "type": "array", + "items": { "type": "string" }, + "example": ["ETH", "USDC", "WETH"] } - }, - "required": ["amount"] + } } } - }, - "required": true + } }, "responses": { "200": { @@ -4737,37 +3817,231 @@ "application/json": { "schema": { "type": "object", - "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, - "data": { - "type": "object", - "properties": { - "nonce": { - "type": "number" - }, - "fee": { - "type": "string" - }, - "amount": { - "type": "string" - }, - "wrappedAddress": { - "type": "string" - }, - "nativeToken": { - "type": "string" - }, - "wrappedToken": { - "type": "string" - } + "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, + "required": ["balances"] + } + } + } + } + } + } + }, + "/chains/ethereum/poll": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Poll Ethereum transaction status", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain-testnet", + "robinhoodchain", + "sepolia", + "unichain" + ], + "type": "string" + }, + "signature": { + "description": "Transaction hash to poll", + "type": "string", + "example": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + } + }, + "required": ["signature"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "currentBlock": { "type": "number" }, + "signature": { "type": "string" }, + "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "txStatus": { + "description": "Transaction status: 1 = confirmed, 0 = pending, -1 = failed, -2 = not found (unknown to the chain: never received or dropped; on Solana this is terminal once the transaction blockhash expires)", + "type": "number" + }, + "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "error": { + "anyOf": [ + { "description": "Error info if failed: \"TYPE (code): message\"", "type": "string" }, + { "type": "null" } + ] + }, + "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] } + }, + "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "error", "txData"] + } + } + } + } + } + } + }, + "/chains/ethereum/allowances": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Get token allowances", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain-testnet", + "robinhoodchain", + "sepolia", + "unichain" + ], + "type": "string" + }, + "address": { + "description": "Ethereum wallet address", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" + }, + "spender": { + "description": "Connector name (e.g., uniswap/clmm, uniswap/amm, 0x/router) or contract address", + "type": "string", + "example": "uniswap/router" + }, + "tokens": { + "description": "Array of token symbols or addresses", + "type": "array", + "items": { "type": "string" }, + "example": ["USDC", "WETH"] + } + }, + "required": ["spender", "tokens"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "spender": { "type": "string" }, + "approvals": { "type": "object", "additionalProperties": { "type": "string" } } + }, + "required": ["spender", "approvals"] + } + } + } + } + } + } + }, + "/chains/ethereum/approve": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Approve token spending", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain-testnet", + "robinhoodchain", + "sepolia", + "unichain" + ], + "type": "string" + }, + "address": { + "description": "Ethereum wallet address", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" + }, + "spender": { + "description": "Connector name (e.g., uniswap/clmm, uniswap/amm, 0x/router) contract address", + "type": "string", + "example": "uniswap/router" + }, + "token": { "description": "Token symbol or address", "type": "string", "example": "USDC" }, + "amount": { + "description": "The amount to approve. If not provided, defaults to maximum amount (unlimited approval).", + "default": "", + "type": "string" + } + }, + "required": ["spender", "token"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "tokenAddress": { "type": "string" }, + "spender": { "type": "string" }, + "amount": { "type": "string" }, + "nonce": { "type": "number" }, + "fee": { "type": "string" } }, - "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + "required": ["tokenAddress", "spender", "amount", "nonce", "fee"] } }, "required": ["signature", "status"] @@ -4778,10 +4052,10 @@ } } }, - "/chains/ethereum/unwrap": { + "/chains/ethereum/wrap": { "post": { "tags": ["/chain/ethereum"], - "description": "Unwrap wrapped token to native token (e.g., WETH to ETH, WBNB to BNB)", + "description": "Wrap native token to wrapped token (e.g., ETH to WETH, BNB to WBNB)", "requestBody": { "content": { "application/json": { @@ -4800,17 +4074,20 @@ "mainnet", "optimism", "polygon", - "sepolia" + "robinhoodchain-testnet", + "robinhoodchain", + "sepolia", + "unichain" ], "type": "string" }, "address": { "description": "Ethereum wallet address", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, "amount": { - "description": "The amount of wrapped token to unwrap (e.g., WETH, WBNB, WAVAX)", + "description": "The amount of native token to wrap (e.g., ETH, BNB, AVAX)", "type": "string", "example": "0.01" } @@ -4829,34 +4106,17 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "nonce": { - "type": "number" - }, - "fee": { - "type": "string" - }, - "amount": { - "type": "string" - }, - "wrappedAddress": { - "type": "string" - }, - "nativeToken": { - "type": "string" - }, - "wrappedToken": { - "type": "string" - } + "nonce": { "type": "number" }, + "fee": { "type": "string" }, + "amount": { "type": "string" }, + "wrappedAddress": { "type": "string" }, + "nativeToken": { "type": "string" }, + "wrappedToken": { "type": "string" } }, "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] } @@ -4869,46 +4129,113 @@ } } }, - "/connectors/jupiter/router/quote-swap": { - "get": { - "tags": ["/connector/jupiter"], - "description": "Get an executable swap quote from Jupiter", - "parameters": [ - { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - }, - { - "schema": { - "type": "string" - }, - "example": "SOL", - "in": "query", - "name": "baseToken", - "required": true, - "description": "Solana token symbol or address to determine swap direction" - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": true, - "description": "The other Solana token symbol or address in the pair" - }, + "/chains/ethereum/unwrap": { + "post": { + "tags": ["/chain/ethereum"], + "description": "Unwrap wrapped token to native token (e.g., WETH to ETH, WBNB to BNB)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The Ethereum network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain-testnet", + "robinhoodchain", + "sepolia", + "unichain" + ], + "type": "string" + }, + "address": { + "description": "Ethereum wallet address", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" + }, + "amount": { + "description": "The amount of wrapped token to unwrap (e.g., WETH, WBNB, WAVAX)", + "type": "string", + "example": "0.01" + } + }, + "required": ["amount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "nonce": { "type": "number" }, + "fee": { "type": "string" }, + "amount": { "type": "string" }, + "wrappedAddress": { "type": "string" }, + "nativeToken": { "type": "string" }, + "wrappedToken": { "type": "string" } + }, + "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/jupiter/router/quote-swap": { + "get": { + "tags": ["/connector/jupiter"], + "description": "Get an executable swap quote from Jupiter", + "parameters": [ { - "schema": { - "type": "number" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Solana token symbol or address to determine swap direction" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": true, + "description": "The other Solana token symbol or address in the pair" + }, + { + "schema": { "type": "number" }, "example": 0.1, "in": "query", "name": "amount", @@ -4916,47 +4243,25 @@ "description": "Amount of base token to trade" }, { - "schema": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 1, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { - "default": true, - "type": "boolean" - }, - "in": "query", - "name": "restrictIntermediateTokens", - "required": false, - "description": "Restrict routing through highly liquid intermediate tokens only for better price and stability" - }, - { - "schema": { - "default": false, - "type": "boolean" - }, + "schema": { "default": true, "type": "boolean" }, "in": "query", - "name": "onlyDirectRoutes", + "name": "approximateIfNoExactOut", "required": false, - "description": "Restrict routing to only go through 1 market" + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing" } ], "responses": { @@ -4967,93 +4272,40 @@ "schema": { "type": "object", "properties": { - "quoteId": { - "description": "Unique identifier for this quote", - "type": "string" - }, - "tokenIn": { - "description": "Address of the token being swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token being swapped to", - "type": "string" - }, - "amountIn": { - "description": "Amount of tokenIn to be swapped", - "type": "number" - }, - "amountOut": { - "description": "Expected amount of tokenOut to receive", - "type": "number" - }, - "price": { - "description": "Exchange rate between tokenIn and tokenOut", - "type": "number" - }, - "priceImpactPct": { - "description": "Estimated price impact percentage (0-100)", - "type": "number" - }, + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { - "description": "Maximum amount of tokenIn that will be spent", - "type": "number" - }, + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, "quoteResponse": { "type": "object", "properties": { - "inputMint": { - "description": "Solana mint address of input token", - "type": "string" - }, - "inAmount": { - "description": "Input amount in token decimals", - "type": "string" - }, - "outputMint": { - "description": "Solana mint address of output token", - "type": "string" - }, - "outAmount": { - "description": "Expected output amount in token decimals", - "type": "string" - }, + "inputMint": { "description": "Solana mint address of input token", "type": "string" }, + "inAmount": { "description": "Input amount in token decimals", "type": "string" }, + "outputMint": { "description": "Solana mint address of output token", "type": "string" }, + "outAmount": { "description": "Expected output amount in token decimals", "type": "string" }, "otherAmountThreshold": { "description": "Minimum output amount based on slippage", "type": "string" }, - "swapMode": { - "description": "Swap mode used (ExactIn or ExactOut)", - "type": "string" - }, - "slippageBps": { - "description": "Slippage in basis points", - "type": "number" - }, - "platformFee": { - "description": "Platform fee information if applicable" - }, - "priceImpactPct": { - "description": "Estimated price impact percentage", - "type": "string" - }, + "swapMode": { "description": "Swap mode used (ExactIn or ExactOut)", "type": "string" }, + "slippageBps": { "description": "Slippage in basis points", "type": "number" }, + "platformFee": { "description": "Platform fee information if applicable" }, + "priceImpactPct": { "description": "Estimated price impact percentage", "type": "string" }, "routePlan": { "description": "Detailed routing plan through various markets", "type": "array", "items": {} }, - "contextSlot": { - "description": "Solana slot used for quote calculation", - "type": "number" - }, - "timeTaken": { - "description": "Time taken to generate quote in milliseconds", - "type": "number" - } + "contextSlot": { "description": "Solana slot used for quote calculation", "type": "number" }, + "timeTaken": { "description": "Time taken to generate quote in milliseconds", "type": "number" } }, "required": [ "inputMint", @@ -5103,7 +4355,7 @@ "properties": { "walletAddress": { "description": "Solana wallet address that will execute the swap", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "network": { @@ -5116,17 +4368,6 @@ "description": "ID of the Jupiter quote to execute", "type": "string", "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "priorityLevel": { - "description": "Priority level for Solana transaction processing", - "enum": ["medium", "high", "veryHigh"], - "default": "veryHigh", - "type": "string" - }, - "maxLamports": { - "description": "Maximum priority fee in lamports for Solana transaction", - "default": [1000000], - "type": "number" } }, "required": ["quoteId"] @@ -5143,10 +4384,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -5154,26 +4392,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -5214,7 +4437,7 @@ "properties": { "walletAddress": { "description": "Solana wallet address that will execute the swap", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "network": { @@ -5233,11 +4456,7 @@ "type": "string", "example": "USDC" }, - "amount": { - "description": "Amount of base token to trade", - "type": "number", - "example": 0.1 - }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -5251,26 +4470,10 @@ "default": 1, "type": "number" }, - "restrictIntermediateTokens": { - "description": "Restrict routing through highly liquid intermediate tokens only for better price and stability", + "approximateIfNoExactOut": { + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing", "default": true, "type": "boolean" - }, - "onlyDirectRoutes": { - "description": "Restrict routing to only go through 1 market", - "default": false, - "type": "boolean" - }, - "priorityLevel": { - "description": "Priority level for Solana transaction processing", - "enum": ["medium", "high", "veryHigh"], - "default": "veryHigh", - "type": "string" - }, - "maxLamports": { - "description": "Maximum priority fee in lamports for Solana transaction", - "default": 1000000, - "type": "number" } }, "required": ["baseToken", "quoteToken", "amount", "side"] @@ -5287,10 +4490,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -5298,26 +4498,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -5346,53 +4531,62 @@ } } }, - "/connectors/meteora/clmm/fetch-pools": { + "/connectors/dflow/router/quote-swap": { "get": { - "tags": ["/connector/meteora"], - "description": "Fetch info about Meteora pools", + "tags": ["/connector/dflow"], + "description": "Get an executable swap quote from DFlow", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "minimum": 1, - "default": 10, - "type": "number" - }, - "example": 10, + "schema": { "type": "string" }, + "example": "SOL", "in": "query", - "name": "limit", - "required": false, - "description": "Maximum number of pools to return" + "name": "baseToken", + "required": true, + "description": "Solana token symbol or address to determine swap direction" }, { - "schema": { - "type": "string" - }, - "example": "SOL", + "schema": { "type": "string" }, + "example": "USDC", "in": "query", - "name": "tokenA", + "name": "quoteToken", + "required": true, + "description": "The other Solana token symbol or address in the pair" + }, + { + "schema": { "type": "number" }, + "example": 0.1, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount of base token to trade" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "in": "query", + "name": "slippagePct", "required": false, - "description": "First token symbol or address" + "description": "Maximum acceptable slippage percentage" }, { - "schema": { - "type": "string" - }, - "example": "USDC", + "schema": { "default": true, "type": "boolean" }, "in": "query", - "name": "tokenB", + "name": "approximateIfNoExactOut", "required": false, - "description": "Second token symbol or address" + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing" } ], "responses": { @@ -5401,84 +4595,80 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" - } + "type": "object", + "properties": { + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", + "type": "number" }, - "required": [ - "address", - "baseTokenAddress", - "quoteTokenAddress", - "feePct", - "price", - "baseTokenAmount", - "quoteTokenAmount", - "activeBinId" - ], - "title": "PoolInfo" - } - } - } + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "approximation": { + "description": "True when a BUY was approximated via a sell-leg ExactIn quote (DFlow is ExactIn-only); amountOut is an estimate", + "type": "boolean" + }, + "quoteResponse": { + "description": "DFlow's native quote response, used to build the swap transaction at execution time" + } + }, + "required": [ + "quoteId", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "priceImpactPct", + "minAmountOut", + "maxAmountIn", + "quoteResponse" + ] + } + } } } } } }, - "/connectors/meteora/clmm/pool-info": { - "get": { - "tags": ["/connector/meteora"], - "description": "Get pool information for a Meteora pool", - "parameters": [ - { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" + "/connectors/dflow/router/execute-quote": { + "post": { + "tags": ["/connector/dflow"], + "description": "Execute a previously fetched quote from DFlow", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["mainnet-beta"], + "type": "string" + }, + "quoteId": { + "description": "ID of the DFlow quote to execute", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + } + }, + "required": ["quoteId"] + } + } }, - { - "schema": { - "type": "string" - }, - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", - "in": "query", - "name": "poolAddress", - "required": true, - "description": "Meteora DLMM pool address" - } - ], + "required": true + }, "responses": { "200": { "description": "Default Response", @@ -5487,79 +4677,146 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" - }, - "dynamicFeePct": { - "type": "number" - }, - "minBinId": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" }, - "maxBinId": { + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/dflow/router/execute-swap": { + "post": { + "tags": ["/connector/dflow"], + "description": "Quote and execute a token swap on DFlow in one step", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["mainnet-beta"], + "type": "string" + }, + "baseToken": { + "description": "Solana token symbol or address to determine swap direction", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "The other Solana token symbol or address in the pair", + "type": "string", + "example": "USDC" + }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, + "side": { + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 1, + "type": "number" + }, + "approximateIfNoExactOut": { + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing", + "default": true, + "type": "boolean" + } + }, + "required": ["baseToken", "quoteToken", "amount", "side"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" }, - "bins": { - "type": "array", - "items": { - "type": "object", - "properties": { - "binId": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - } + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" }, - "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"], - "title": "BinLiquidity" - } + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] } }, - "required": [ - "address", - "baseTokenAddress", - "quoteTokenAddress", - "feePct", - "price", - "baseTokenAmount", - "quoteTokenAmount", - "activeBinId", - "dynamicFeePct", - "minBinId", - "maxBinId", - "bins" - ] + "required": ["signature", "status"] } } } @@ -5567,304 +4824,62 @@ } } }, - "/connectors/meteora/clmm/positions-owned": { + "/connectors/okx/router/quote-swap": { "get": { - "tags": ["/connector/meteora"], - "description": "Retrieve all positions owned by a user's wallet across all Meteora pools", + "tags": ["/connector/okx"], + "description": "Get an executable swap quote from the OKX DEX aggregator", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "schema": { "type": "string" }, + "example": "SOL", "in": "query", - "name": "walletAddress", + "name": "baseToken", "required": true, - "description": "Solana wallet address to check for positions" - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } - }, - "required": [ - "address", - "poolAddress", - "baseTokenAddress", - "quoteTokenAddress", - "baseTokenAmount", - "quoteTokenAmount", - "baseFeeAmount", - "quoteFeeAmount", - "lowerBinId", - "upperBinId", - "lowerPrice", - "upperPrice", - "price" - ] - } - } - } - } - } - } - } - }, - "/connectors/meteora/clmm/position-info": { - "get": { - "tags": ["/connector/meteora"], - "description": "Get details for a specific Meteora position", - "parameters": [ - { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - }, - { - "schema": { - "type": "string" - }, - "example": "\u003Csample-position-address\u003E", - "in": "query", - "name": "positionAddress", - "required": true, - "description": "Position NFT address" - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } - }, - "required": [ - "address", - "poolAddress", - "baseTokenAddress", - "quoteTokenAddress", - "baseTokenAmount", - "quoteTokenAmount", - "baseFeeAmount", - "quoteFeeAmount", - "lowerBinId", - "upperBinId", - "lowerPrice", - "upperPrice", - "price" - ] - } - } - } - } - } - } - }, - "/connectors/meteora/clmm/quote-position": { - "get": { - "tags": ["/connector/meteora"], - "description": "Quote amounts for a new Meteora CLMM position", - "parameters": [ - { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" + "description": "Solana token symbol or address to determine swap direction" }, { - "schema": { - "type": "number" - }, - "example": 150, + "schema": { "type": "string" }, + "example": "USDC", "in": "query", - "name": "lowerPrice", + "name": "quoteToken", "required": true, - "description": "Lower price bound for the position" + "description": "The other Solana token symbol or address in the pair" }, { - "schema": { - "type": "number" - }, - "example": 250, + "schema": { "type": "number" }, + "example": 0.1, "in": "query", - "name": "upperPrice", + "name": "amount", "required": true, - "description": "Upper price bound for the position" + "description": "Amount of base token to trade" }, { - "schema": { - "type": "string" - }, - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "in": "query", - "name": "poolAddress", + "name": "side", "required": true, - "description": "Meteora DLMM pool address" - }, - { - "schema": { - "type": "number" - }, - "example": 0.01, - "in": "query", - "name": "baseTokenAmount", - "required": false, - "description": "Amount of base token to deposit" - }, - { - "schema": { - "type": "number" - }, - "example": 2, - "in": "query", - "name": "quoteTokenAmount", - "required": false, - "description": "Amount of quote token to deposit" + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, - "example": 2, + "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { - "enum": [0, 1, 2], - "type": "number" - }, - "example": 0, + "schema": { "default": true, "type": "boolean" }, "in": "query", - "name": "strategyType", + "name": "approximateIfNoExactOut", "required": false, - "description": "Strategy type for the position" + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing" } ], "responses": { @@ -5875,29 +4890,37 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "quoteTokenAmountMax": { - "type": "number" + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "approximation": { + "description": "True when a BUY was approximated via a sell-leg exactIn quote because exactOut was unavailable; amountOut is an estimate", + "type": "boolean" }, - "liquidity": {} + "routerResult": { + "description": "OKX's native quote result (amounts, price impact, routing breakdown)" + } }, "required": [ - "baseLimited", - "baseTokenAmount", - "quoteTokenAmount", - "baseTokenAmountMax", - "quoteTokenAmountMax" + "quoteId", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "priceImpactPct", + "minAmountOut", + "maxAmountIn", + "routerResult" ] } } @@ -5906,204 +4929,34 @@ } } }, - "/connectors/meteora/clmm/quote-swap": { - "get": { - "tags": ["/connector/meteora"], - "description": "Get swap quote for Meteora CLMM", - "parameters": [ - { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, - "in": "query", - "name": "network", - "required": false, - "description": "Solana network to use" - }, - { - "schema": { - "type": "string" - }, - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", - "in": "query", - "name": "poolAddress", - "required": false, - "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)" - }, - { - "schema": { - "type": "string" - }, - "example": "SOL", - "in": "query", - "name": "baseToken", - "required": true, - "description": "Token to determine swap direction" - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false, - "description": "The other token in the pair (optional - required if poolAddress not provided)" - }, - { - "schema": { - "type": "number" - }, - "example": 0.01, - "in": "query", - "name": "amount", - "required": true, - "description": "Amount to swap" - }, - { - "schema": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, - "example": "SELL", - "in": "query", - "name": "side", - "required": true, - "description": "Trade direction" - }, - { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, - "example": 2, - "in": "query", - "name": "slippagePct", - "required": false, - "description": "Maximum acceptable slippage percentage" - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } - }, - "required": [ - "poolAddress", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "minAmountOut", - "maxAmountIn", - "priceImpactPct" - ] - } - } - } - } - } - } - }, - "/connectors/meteora/clmm/execute-swap": { + "/connectors/okx/router/execute-quote": { "post": { - "tags": ["/connector/meteora"], - "description": "Execute a token swap on Meteora DLMM", + "tags": ["/connector/okx"], + "description": "Execute a previously fetched quote from the OKX DEX aggregator", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "network": { "description": "Solana network to use", "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "enum": ["mainnet-beta"], "type": "string" }, - "walletAddress": { - "description": "Solana wallet address that will execute the swap", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", - "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" - }, - "poolAddress": { - "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)", - "type": "string", - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" - }, - "baseToken": { - "description": "Base token symbol or address", - "type": "string", - "example": "SOL" - }, - "quoteToken": { - "description": "Quote token symbol or address (optional - required if poolAddress not provided)", - "type": "string", - "example": "USDC" - }, - "amount": { - "description": "Amount to swap", - "type": "number", - "example": 0.01 - }, - "side": { - "description": "Trade direction", - "enum": ["BUY", "SELL"], - "default": "SELL", + "quoteId": { + "description": "ID of the OKX quote to execute", "type": "string", - "example": "SELL" - }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage", - "default": 2, - "type": "number", - "example": 2 + "example": "123e4567-e89b-12d3-a456-426614174000" } }, - "required": ["baseToken", "amount", "side"] + "required": ["quoteId"] } } }, @@ -6117,35 +4970,25 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { - "description": "TransactionStatus enum value", + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" }, "data": { "type": "object", "properties": { - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "fee": { - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", "type": "number" }, "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", "type": "number" } }, @@ -6168,69 +5011,58 @@ } } }, - "/connectors/meteora/clmm/open-position": { + "/connectors/okx/router/execute-swap": { "post": { - "tags": ["/connector/meteora"], - "description": "Open a new Meteora position", + "tags": ["/connector/okx"], + "description": "Quote and execute a token swap on the OKX DEX aggregator in one step", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "network": { "description": "Solana network to use", "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], + "enum": ["mainnet-beta"], "type": "string" }, - "walletAddress": { - "description": "Solana wallet address that will open the position", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "baseToken": { + "description": "Solana token symbol or address to determine swap direction", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" - }, - "lowerPrice": { - "description": "Lower price bound for the position", - "type": "number", - "example": 150 - }, - "upperPrice": { - "description": "Upper price bound for the position", - "type": "number", - "example": 250 + "example": "SOL" }, - "poolAddress": { - "description": "Meteora DLMM pool address", + "quoteToken": { + "description": "The other Solana token symbol or address in the pair", "type": "string", - "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" - }, - "baseTokenAmount": { - "description": "Amount of base token to deposit", - "type": "number", - "example": 0.01 + "example": "USDC" }, - "quoteTokenAmount": { - "description": "Amount of quote token to deposit", - "type": "number", - "example": 2 + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, + "side": { + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" }, "slippagePct": { "minimum": 0, "maximum": 100, "description": "Maximum acceptable slippage percentage", - "default": 2, - "type": "number", - "example": 2 + "default": 1, + "type": "number" }, - "strategyType": { - "description": "Strategy type for the position", - "enum": [0, 1, 2], - "type": "number", - "example": 0 + "approximateIfNoExactOut": { + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing", + "default": true, + "type": "boolean" } }, - "required": ["lowerPrice", "upperPrice", "poolAddress"] + "required": ["baseToken", "quoteToken", "amount", "side"] } } }, @@ -6244,42 +5076,1801 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { - "description": "TransactionStatus enum value", + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionAddress": { - "type": "string" - }, - "positionRent": { - "type": "number" - }, - "baseTokenAmountAdded": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", "type": "number" }, - "quoteTokenAmountAdded": { + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", "type": "number" } }, "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", "fee", - "positionAddress", - "positionRent", - "baseTokenAmountAdded", - "quoteTokenAmountAdded" + "baseTokenBalanceChange", + "quoteTokenBalanceChange" ] } }, - "required": ["signature", "status"] + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/titan/router/quote-swap": { + "get": { + "tags": ["/connector/titan"], + "description": "Get an executable swap quote from Titan (DART)", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Solana token symbol or address to determine swap direction" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": true, + "description": "The other Solana token symbol or address in the pair" + }, + { + "schema": { "type": "number" }, + "example": 0.1, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount of base token to trade" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + }, + { + "schema": { "default": true, "type": "boolean" }, + "in": "query", + "name": "approximateIfNoExactOut", + "required": false, + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing" + }, + { + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": false, + "description": "Solana wallet the quote instructions are built for (Titan quotes are wallet-bound)" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { + "description": "Estimated price impact percentage (0-100); Titan DART does not report price impact, so this is 0", + "type": "number" + }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", + "type": "number" + }, + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "approximation": { + "description": "True when a BUY was approximated via a sell-leg ExactIn quote (Titan DART is ExactIn-only); amountOut is an estimate", + "type": "boolean" + }, + "wallet": { + "description": "Wallet address this quote is bound to; execute-quote must be called with the same wallet or it will fail", + "type": "string" + } + }, + "required": [ + "quoteId", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "priceImpactPct", + "minAmountOut", + "maxAmountIn", + "wallet" + ] + } + } + } + } + } + } + }, + "/connectors/titan/router/execute-quote": { + "post": { + "tags": ["/connector/titan"], + "description": "Execute a previously fetched quote from Titan (DART)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap (must match the wallet the quote was created for)", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["mainnet-beta"], + "type": "string" + }, + "quoteId": { + "description": "ID of the Titan quote to execute", + "type": "string", + "example": "123e4567-e89b-12d3-a456-426614174000" + } + }, + "required": ["quoteId"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/titan/router/execute-swap": { + "post": { + "tags": ["/connector/titan"], + "description": "Quote and execute a token swap on Titan (DART) in one step", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["mainnet-beta"], + "type": "string" + }, + "baseToken": { + "description": "Solana token symbol or address to determine swap direction", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "The other Solana token symbol or address in the pair", + "type": "string", + "example": "USDC" + }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, + "side": { + "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 1, + "type": "number" + }, + "approximateIfNoExactOut": { + "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing", + "default": true, + "type": "boolean" + } + }, + "required": ["baseToken", "quoteToken", "amount", "side"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, + "status": { + "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/fetch-pools": { + "get": { + "tags": ["/connector/meteora"], + "description": "Fetch Meteora pools from API with search and sorting", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "minimum": 0, "default": 0, "type": "number" }, + "example": 0, + "in": "query", + "name": "page", + "required": false, + "description": "Page number (0-based)" + }, + { + "schema": { "minimum": 1, "maximum": 1000, "default": 50, "type": "number" }, + "example": 50, + "in": "query", + "name": "limit", + "required": false, + "description": "Maximum number of pools to return (max 1000)" + }, + { + "schema": { "type": "string" }, + "examples": { "SOL": { "value": "SOL" }, "USDC": { "value": "USDC" }, "SOL-USDC": { "value": "SOL-USDC" } }, + "in": "query", + "name": "query", + "required": false, + "description": "Search query to match pools by name, tokens, or address" + }, + { + "schema": { "default": "volume_24h:desc", "type": "string" }, + "examples": { + "volume_24h:desc": { "value": "volume_24h:desc" }, + "tvl:desc": { "value": "tvl:desc" }, + "apr:desc": { "value": "apr:desc" } + }, + "in": "query", + "name": "sortBy", + "required": false, + "description": "Sort by field (volume, fees, tvl, apr) with optional time window" + }, + { + "schema": { "default": true, "type": "boolean" }, + "in": "query", + "name": "includeUnverified", + "required": false, + "description": "Include pools with unverified tokens" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pools": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "description": "Pool address", "type": "string" }, + "name": { "description": "Pool name (e.g., SOL-USDC)", "type": "string" }, + "baseTokenAddress": { "description": "Base token address", "type": "string" }, + "baseTokenSymbol": { "description": "Base token symbol", "type": "string" }, + "quoteTokenAddress": { "description": "Quote token address", "type": "string" }, + "quoteTokenSymbol": { "description": "Quote token symbol", "type": "string" }, + "binStep": { "description": "Bin step / tick spacing", "type": "number" }, + "baseFee": { "description": "Base fee percentage", "type": "number" }, + "price": { "description": "Current price", "type": "number" }, + "tvl": { "description": "Total value locked in USD", "type": "number" }, + "apr": { "description": "Annual percentage rate", "type": "number" }, + "apy": { "description": "Annual percentage yield", "type": "number" }, + "volume24h": { "description": "24-hour trading volume", "type": "number" }, + "fees24h": { "description": "24-hour fees collected", "type": "number" } + }, + "required": [ + "address", + "name", + "baseTokenAddress", + "baseTokenSymbol", + "quoteTokenAddress", + "quoteTokenSymbol", + "binStep", + "baseFee", + "price", + "tvl" + ], + "title": "PoolListItem" + } + }, + "total": { "description": "Total number of matching pools", "type": "number" }, + "page": { "description": "Current page number", "type": "number" }, + "pageSize": { "description": "Number of pools per page", "type": "number" } + }, + "required": ["pools", "total", "page", "pageSize"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/create-pool": { + "post": { + "tags": ["/connector/meteora"], + "description": "Create and initialize a new Meteora DLMM pool (LB pair) at an initial price (no liquidity seeded)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will create the pool", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, + "initialPrice": { + "description": "Initial price as quote per base (e.g. USDC per SOL). Encodes the pool active bin.", + "type": "number", + "example": 250 + }, + "binStep": { + "description": "Bin step in basis points (e.g. 1, 2, 4, 5, 10, 20, 25, 50, 100). Sets pool granularity; cannot be changed after creation.", + "type": "number", + "example": 20 + }, + "feeBps": { + "description": "Base swap fee in basis points (e.g. 20 = 0.20%). Must be compatible with binStep.", + "type": "number", + "example": 20 + } + }, + "required": ["baseToken", "quoteToken", "initialPrice", "binStep", "feeBps"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was initialized at (quote per base)", + "type": "number" + }, + "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + }, + "required": ["signature", "status", "poolAddress"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/pool-info": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get pool information for a Meteora pool", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Meteora DLMM pool address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] + } + }, + "dynamicFeePct": { "type": "number" }, + "minBinId": { "type": "number" }, + "maxBinId": { "type": "number" } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount", + "activeBinId", + "dynamicFeePct", + "minBinId", + "maxBinId" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/positions-owned": { + "get": { + "tags": ["/connector/meteora"], + "description": "Retrieve all positions owned by a user's wallet across all Meteora pools", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "in": "query", + "name": "walletAddress", + "required": true, + "description": "Solana wallet address to check for positions" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/position-info": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get details for a specific Meteora position", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "", + "in": "query", + "name": "positionAddress", + "required": true, + "description": "Position NFT address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", + "baseFeeAmount", + "quoteFeeAmount", + "lowerBinId", + "upperBinId", + "lowerPrice", + "upperPrice", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/quote-position": { + "get": { + "tags": ["/connector/meteora"], + "description": "Quote amounts for a new Meteora CLMM position", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "number" }, + "example": 150, + "in": "query", + "name": "lowerPrice", + "required": true, + "description": "Lower price bound for the position" + }, + { + "schema": { "type": "number" }, + "example": 250, + "in": "query", + "name": "upperPrice", + "required": true, + "description": "Upper price bound for the position" + }, + { + "schema": { "type": "string" }, + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Meteora DLMM pool address" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "baseTokenAmount", + "required": false, + "description": "Amount of base token to deposit" + }, + { + "schema": { "type": "number" }, + "example": 2, + "in": "query", + "name": "quoteTokenAmount", + "required": false, + "description": "Amount of quote token to deposit" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + }, + { + "schema": { "enum": [0, 1, 2], "type": "number" }, + "example": 0, + "in": "query", + "name": "strategyType", + "required": false, + "description": "Strategy type for the position" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, + "liquidity": {} + }, + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/quote-swap": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get swap quote for Meteora CLMM", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", + "in": "query", + "name": "poolAddress", + "required": false, + "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Token to determine swap direction" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false, + "description": "The other token in the pair (optional - required if poolAddress not provided)" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount to swap" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "example": "SELL", + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } + }, + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/execute-swap": { + "post": { + "tags": ["/connector/meteora"], + "description": "Execute a token swap on Meteora DLMM", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "poolAddress": { + "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)", + "type": "string", + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" + }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "quoteToken": { + "description": "Quote token symbol or address (optional - required if poolAddress not provided)", + "type": "string", + "example": "USDC" + }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, + "side": { + "description": "Trade direction", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string", + "example": "SELL" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + } + }, + "required": ["baseToken", "amount", "side"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/open-position": { + "post": { + "tags": ["/connector/meteora"], + "description": "Open a new Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will open the position", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "lowerPrice": { + "description": "Lower price bound for the position", + "type": "number", + "example": 150 + }, + "upperPrice": { + "description": "Upper price bound for the position", + "type": "number", + "example": 250 + }, + "poolAddress": { + "description": "Meteora DLMM pool address", + "type": "string", + "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" + }, + "baseTokenAmount": { + "description": "Amount of base token to deposit", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to deposit", + "type": "number", + "example": 2 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + }, + "strategyType": { + "description": "Strategy type for the position", + "enum": [0, 1, 2], + "type": "number", + "example": 0 + } + }, + "required": ["lowerPrice", "upperPrice", "poolAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": [ + "fee", + "positionAddress", + "positionRent", + "baseTokenAmountAdded", + "quoteTokenAmountAdded" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/add-liquidity": { + "post": { + "tags": ["/connector/meteora"], + "description": "Add liquidity to a Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will add liquidity", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + }, + "baseTokenAmount": { + "description": "Amount of base token to deposit", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to deposit", + "type": "number", + "example": 2 + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number", + "example": 2 + }, + "strategyType": { + "description": "Strategy type for the position", + "enum": [0, 1, 2], + "type": "number", + "example": 0 + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/remove-liquidity": { + "post": { + "tags": ["/connector/meteora"], + "description": "Remove liquidity from a Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will remove liquidity", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + }, + "percentageToRemove": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of liquidity to remove", + "default": 100, + "type": "number", + "example": 100 + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/collect-fees": { + "post": { + "tags": ["/connector/meteora"], + "description": "Collect fees from a Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will collect fees", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/clmm/close-position": { + "post": { + "tags": ["/connector/meteora"], + "description": "Close a Meteora position", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will close the position", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + } + }, + "required": ["positionAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } + }, + "required": [ + "fee", + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", + "quoteFeeAmountCollected" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/meteora/amm/pool-info": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get AMM pool information from Meteora DAMM v2", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Meteora DAMM v2 pool address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", + "feePct", + "price", + "baseTokenAmount", + "quoteTokenAmount" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/amm/position-info": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get the wallet's aggregated liquidity in a Meteora DAMM v2 pool. DAMM v2 positions are NFTs; amounts sum across all of the wallet positions in the pool.", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Meteora DAMM v2 pool address" + }, + { + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": false, + "description": "Solana wallet address" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "positionAddress": { + "description": "Address of the individual position (NFT position account)", + "type": "string" + }, + "lpTokenAmount": { + "description": "Liquidity held by this position (LP units)", + "type": "number" + }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] + } + } + }, + "required": [ + "poolAddress", + "walletAddress", + "baseTokenAddress", + "quoteTokenAddress", + "lpTokenAmount", + "baseTokenAmount", + "quoteTokenAmount", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/amm/positions-owned": { + "get": { + "tags": ["/connector/meteora"], + "description": "List all of a wallet's DAMM v2 positions across all Meteora AMM pools", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "in": "query", + "name": "walletAddress", + "required": true, + "description": "Solana wallet address to list DAMM v2 positions for" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "positionAddress": { + "description": "Address of the individual position (NFT position account)", + "type": "string" + }, + "lpTokenAmount": { + "description": "Liquidity held by this position (LP units)", + "type": "number" + }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] + } + } + }, + "required": [ + "poolAddress", + "walletAddress", + "baseTokenAddress", + "quoteTokenAddress", + "lpTokenAmount", + "baseTokenAmount", + "quoteTokenAmount", + "price" + ] + } + } + } + } + } + } + } + }, + "/connectors/meteora/amm/quote-swap": { + "get": { + "tags": ["/connector/meteora"], + "description": "Get a swap quote for a Meteora DAMM v2 pool", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Meteora DAMM v2 pool address" + }, + { + "schema": { "type": "string" }, + "example": "SOL", + "in": "query", + "name": "baseToken", + "required": true, + "description": "Token to determine swap direction" + }, + { + "schema": { "type": "string" }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false, + "description": "The other token in the pair (optional - resolved from the pool if omitted)" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "amount", + "required": true, + "description": "Amount to swap (denominated in the base token)" + }, + { + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "in": "query", + "name": "side", + "required": true, + "description": "Trade direction" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } + }, + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] + } + } + } + } + } + } + }, + "/connectors/meteora/amm/quote-liquidity": { + "get": { + "tags": ["/connector/meteora"], + "description": "Quote amounts for adding liquidity to a Meteora DAMM v2 pool", + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "in": "query", + "name": "network", + "required": false, + "description": "Solana network to use" + }, + { + "schema": { "type": "string" }, + "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv", + "in": "query", + "name": "poolAddress", + "required": true, + "description": "Meteora DAMM v2 pool address" + }, + { + "schema": { "type": "number" }, + "example": 0.01, + "in": "query", + "name": "baseTokenAmount", + "required": true, + "description": "Amount of base token to add" + }, + { + "schema": { "type": "number" }, + "example": 2, + "in": "query", + "name": "quoteTokenAmount", + "required": true, + "description": "Amount of quote token to add" + }, + { + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "example": 2, + "in": "query", + "name": "slippagePct", + "required": false, + "description": "Maximum acceptable slippage percentage" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" } + }, + "required": [ + "baseLimited", + "baseTokenAmount", + "quoteTokenAmount", + "baseTokenAmountMax", + "quoteTokenAmountMax" + ] } } } @@ -6287,42 +6878,48 @@ } } }, - "/connectors/meteora/clmm/add-liquidity": { + "/connectors/meteora/amm/execute-swap": { "post": { "tags": ["/connector/meteora"], - "description": "Add liquidity to a Meteora position", + "description": "Execute a swap on a Meteora DAMM v2 pool", "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { + "walletAddress": { + "description": "Solana wallet address that will execute the swap", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "network": { "description": "Solana network to use", "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, - "walletAddress": { - "description": "Solana wallet address that will add liquidity", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "poolAddress": { + "description": "Meteora DAMM v2 pool address", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv" }, - "positionAddress": { - "description": "Position NFT address", + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "quoteToken": { + "description": "The other token in the pair (optional - resolved from the pool if omitted)", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "USDC" }, - "baseTokenAmount": { - "description": "Amount of base token to deposit", + "amount": { + "description": "Amount to swap (denominated in the base token)", "type": "number", "example": 0.01 }, - "quoteTokenAmount": { - "description": "Amount of quote token to deposit", - "type": "number", - "example": 2 + "side": { + "description": "Trade direction", + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" }, "slippagePct": { "minimum": 0, @@ -6331,15 +6928,9 @@ "default": 2, "type": "number", "example": 2 - }, - "strategyType": { - "description": "Strategy type for the position", - "enum": [0, 1, 2], - "type": "number", - "example": 0 } }, - "required": ["positionAddress"] + "required": ["poolAddress", "baseToken", "amount", "side"] } } }, @@ -6353,27 +6944,28 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } }, - "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] } }, "required": ["signature", "status"] @@ -6384,10 +6976,10 @@ } } }, - "/connectors/meteora/clmm/remove-liquidity": { + "/connectors/meteora/amm/add-liquidity": { "post": { "tags": ["/connector/meteora"], - "description": "Remove liquidity from a Meteora position", + "description": "Add liquidity to a Meteora DAMM v2 pool. Provide positionAddress to add to a specific position (NFT); omit it to open a new position.", "requestBody": { "content": { "application/json": { @@ -6401,26 +6993,35 @@ "type": "string" }, "walletAddress": { - "description": "Solana wallet address that will remove liquidity", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "description": "Solana wallet address", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "poolAddress": { + "description": "Meteora DAMM v2 pool address", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv" + }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number", + "example": 0.01 }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, "positionAddress": { - "description": "Position NFT address", - "type": "string", - "example": "\u003Csample-position-address\u003E" + "description": "DAMM v2 positions are NFTs; a wallet may hold several per pool. Provide a position address (from position-info) to add to that specific position; omit to open a NEW position NFT.", + "type": "string" }, - "liquidityPct": { + "slippagePct": { "minimum": 0, "maximum": 100, - "description": "Percentage of liquidity to remove", - "default": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, "type": "number", - "example": 100 + "example": 2 } }, - "required": ["positionAddress"] + "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] } } }, @@ -6434,27 +7035,16 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, - "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } }, "required": ["signature", "status"] @@ -6465,10 +7055,10 @@ } } }, - "/connectors/meteora/clmm/collect-fees": { + "/connectors/meteora/amm/remove-liquidity": { "post": { "tags": ["/connector/meteora"], - "description": "Collect fees from a Meteora position", + "description": "Remove liquidity from a specific position (NFT) in a Meteora DAMM v2 pool", "requestBody": { "content": { "application/json": { @@ -6482,18 +7072,28 @@ "type": "string" }, "walletAddress": { - "description": "Solana wallet address that will collect fees", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "description": "Solana wallet address", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "poolAddress": { + "description": "Meteora DAMM v2 pool address", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv" }, "positionAddress": { - "description": "Position NFT address", - "type": "string", - "example": "\u003Csample-position-address\u003E" + "description": "Address of the specific DAMM v2 position (NFT) to remove from. Required — a wallet may hold several positions per pool; list them with position-info. This avoids silently draining only the largest position when several exist.", + "type": "string" + }, + "percentageToRemove": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of this position’s liquidity to remove", + "type": "number", + "example": 100 } }, - "required": ["positionAddress"] + "required": ["poolAddress", "positionAddress", "percentageToRemove"] } } }, @@ -6507,27 +7107,16 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, - "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } }, "required": ["signature", "status"] @@ -6538,10 +7127,10 @@ } } }, - "/connectors/meteora/clmm/close-position": { + "/connectors/meteora/amm/create-pool": { "post": { "tags": ["/connector/meteora"], - "description": "Close a Meteora position", + "description": "Create a new Meteora DAMM v2 pool and seed it with initial liquidity", "requestBody": { "content": { "application/json": { @@ -6555,18 +7144,40 @@ "type": "string" }, "walletAddress": { - "description": "Solana wallet address that will close the position", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "description": "Solana wallet address that will create and seed the pool", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "baseToken": { + "description": "Base token symbol or address (becomes pool token A)", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "SOL" }, - "positionAddress": { - "description": "Position NFT address", + "quoteToken": { + "description": "Quote token symbol or address (becomes pool token B)", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "USDC" + }, + "baseTokenAmount": { + "description": "Amount of base token to seed the pool with", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. If omitted (and no initialPrice), the current market price is fetched from the swap router.", + "type": "number", + "example": 2 + }, + "initialPrice": { + "description": "Initial price as quote per base (e.g. SOL per UMBRA). Overrides quoteTokenAmount. If both are omitted, the pool is seeded at the current market price so it is not immediately arbitraged.", + "type": "number" + }, + "configAddress": { + "description": "DAMM v2 config account that defines the fee tier and pool parameters. Required — many permissionless configs are launch configs with very high starting fees, so Gateway does not auto-select one.", + "type": "string" } }, - "required": ["positionAddress"] + "required": ["baseToken", "quoteToken", "baseTokenAmount"] } } }, @@ -6580,46 +7191,24 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was seeded at (quote per base)", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionRentRefunded": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, - "required": [ - "fee", - "positionRentRefunded", - "baseTokenAmountRemoved", - "quoteTokenAmountRemoved", - "baseFeeAmountCollected", - "quoteFeeAmountCollected" - ] + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } }, - "required": ["signature", "status"] + "required": ["signature", "status", "poolAddress"] } } } @@ -6630,50 +7219,55 @@ "/connectors/orca/clmm/fetch-pools": { "get": { "tags": ["/connector/orca"], - "description": "Fetch info about Orca pools", + "description": "Fetch Orca pools from API with search and sorting", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "minimum": 1, - "default": 10, - "type": "number" - }, - "example": 10, + "schema": { "minimum": 1, "maximum": 100, "default": 50, "type": "number" }, + "example": 50, "in": "query", "name": "limit", "required": false, "description": "Maximum number of pools to return" }, { - "schema": { - "type": "string" - }, - "example": "SOL", + "schema": { "type": "string" }, + "examples": { "SOL": { "value": "SOL" }, "USDC": { "value": "USDC" }, "SOL-USDC": { "value": "SOL-USDC" } }, "in": "query", - "name": "tokenA", + "name": "query", "required": false, - "description": "First token symbol or address" + "description": "Search query to match pools by name, tokens, or address" }, { "schema": { + "enum": ["volume", "tvl", "fees", "rewards", "yieldovertvl"], + "default": "volume", "type": "string" }, - "example": "USDC", "in": "query", - "name": "tokenB", + "name": "sortBy", "required": false, - "description": "Second token symbol or address" + "description": "Sort by field" + }, + { + "schema": { "enum": ["asc", "desc"], "default": "desc", "type": "string" }, + "in": "query", + "name": "sortDirection", + "required": false, + "description": "Sort direction" + }, + { + "schema": { "default": false, "type": "boolean" }, + "in": "query", + "name": "verifiedOnly", + "required": false, + "description": "Only return pools with verified tokens" } ], "responses": { @@ -6682,49 +7276,47 @@ "content": { "application/json": { "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" + "type": "object", + "properties": { + "pools": { + "type": "array", + "items": { + "type": "object", + "properties": { + "address": { "description": "Pool address", "type": "string" }, + "name": { "description": "Pool name (e.g., SOL-USDC)", "type": "string" }, + "baseTokenAddress": { "description": "Base token address", "type": "string" }, + "baseTokenSymbol": { "description": "Base token symbol", "type": "string" }, + "quoteTokenAddress": { "description": "Quote token address", "type": "string" }, + "quoteTokenSymbol": { "description": "Quote token symbol", "type": "string" }, + "binStep": { "description": "Bin step / tick spacing", "type": "number" }, + "baseFee": { "description": "Base fee percentage", "type": "number" }, + "price": { "description": "Current price", "type": "number" }, + "tvl": { "description": "Total value locked in USD", "type": "number" }, + "apr": { "description": "Annual percentage rate", "type": "number" }, + "apy": { "description": "Annual percentage yield", "type": "number" }, + "volume24h": { "description": "24-hour trading volume", "type": "number" }, + "fees24h": { "description": "24-hour fees collected", "type": "number" } + }, + "required": [ + "address", + "name", + "baseTokenAddress", + "baseTokenSymbol", + "quoteTokenAddress", + "quoteTokenSymbol", + "binStep", + "baseFee", + "price", + "tvl" + ] } }, - "required": [ - "address", - "baseTokenAddress", - "quoteTokenAddress", - "feePct", - "price", - "baseTokenAmount", - "quoteTokenAmount", - "activeBinId" - ] - } + "total": { "description": "Total number of matching pools", "type": "number" }, + "page": { "description": "Current page number", "type": "number" }, + "pageSize": { "description": "Number of pools per page", "type": "number" } + }, + "required": ["pools", "total", "page", "pageSize"] } } } @@ -6738,25 +7330,26 @@ "description": "Get pool information for a Orca pool", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" - }, - { - "schema": { - "type": "string" - }, + }, + { + "schema": { "type": "string" }, "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", "in": "query", "name": "poolAddress", "required": true, "description": "Orca CLMM pool address" + }, + { + "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "in": "query", + "name": "binCount", + "required": false, + "description": "If > 0, include a `bins` array (per-tickSpacing token amounts around the current tick). Default 0 — pool-info skips the extra getProgramAccounts call." } ], "responses": { @@ -6767,48 +7360,33 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" - }, - "liquidity": { - "type": "string" - }, - "sqrtPrice": { - "type": "string" - }, - "tvlUsdc": { - "type": "number" - }, - "protocolFeeRate": { - "type": "number" + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] + } }, - "yieldOverTvl": { - "type": "number" - } + "liquidity": { "type": "string" }, + "sqrtPrice": { "type": "string" }, + "tvlUsdc": { "type": "number" }, + "protocolFeeRate": { "type": "number" }, + "yieldOverTvl": { "type": "number" } }, "required": [ "address", @@ -6835,39 +7413,22 @@ "/connectors/orca/clmm/positions-owned": { "get": { "tags": ["/connector/orca"], - "description": "Retrieve a list of positions owned by a user's wallet in a specific Orca pool", + "description": "Retrieve all positions owned by a user's wallet across Orca CLMM pools", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", - "type": "string" - }, - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", "required": false, "description": "Solana wallet address to check for positions" - }, - { - "schema": { - "type": "string" - }, - "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", - "in": "query", - "name": "poolAddress", - "required": true, - "description": "Orca CLMM pool address" } ], "responses": { @@ -6880,51 +7441,19 @@ "items": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -6955,32 +7484,23 @@ "description": "Get details for a specific Orca position", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, - "example": "\u003Csample-position-address\u003E", + "schema": { "type": "string" }, + "example": "", "in": "query", "name": "positionAddress", "required": true, "description": "Position address" }, { - "schema": { - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", - "type": "string" - }, - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", "required": false, @@ -6995,51 +7515,19 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -7069,20 +7557,14 @@ "description": "Quote amounts for a new Orca CLMM position", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 200, "in": "query", "name": "lowerPrice", @@ -7090,9 +7572,7 @@ "description": "Lower price bound for the position" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 300, "in": "query", "name": "upperPrice", @@ -7100,9 +7580,7 @@ "description": "Upper price bound for the position" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", "in": "query", "name": "poolAddress", @@ -7110,9 +7588,7 @@ "description": "Orca CLMM pool address" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -7120,9 +7596,7 @@ "description": "Amount of base token to deposit" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -7130,12 +7604,7 @@ "description": "Amount of quote token to deposit" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 1, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", @@ -7151,21 +7620,11 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - }, + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, "liquidity": {} }, "required": [ @@ -7188,20 +7647,14 @@ "description": "Get swap quote for Orca CLMM", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", "in": "query", "name": "poolAddress", @@ -7209,9 +7662,7 @@ "description": "Orca CLMM pool address (optional - can be looked up from baseToken and quoteToken)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "SOL", "in": "query", "name": "baseToken", @@ -7219,9 +7670,7 @@ "description": "Token to determine swap direction" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -7229,9 +7678,7 @@ "description": "The other token in the pair (optional - required if poolAddress not provided)" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "amount", @@ -7239,11 +7686,7 @@ "description": "Amount to swap" }, { - "schema": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "example": "SELL", "in": "query", "name": "side", @@ -7251,12 +7694,7 @@ "description": "Trade direction" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 1, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", @@ -7272,36 +7710,16 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ "poolAddress", @@ -7339,30 +7757,22 @@ }, "walletAddress": { "description": "Solana wallet address that will execute the swap", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, "poolAddress": { "description": "Orca CLMM pool address (optional - can be looked up from baseToken and quoteToken)", "type": "string", "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE" }, - "baseToken": { - "description": "Base token symbol or address", - "type": "string", - "example": "SOL" - }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, "quoteToken": { "description": "Quote token symbol or address (optional - required if poolAddress not provided)", "type": "string", "example": "USDC" }, - "amount": { - "description": "Amount to swap", - "type": "number", - "example": 0.01 - }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], @@ -7393,37 +7803,18 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "fee": { - "type": "number" - }, - "baseTokenBalanceChange": { - "type": "number" - }, - "quoteTokenBalanceChange": { - "type": "number" - } + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } }, "required": [ "tokenIn", @@ -7436,7 +7827,82 @@ ] } }, - "required": ["signature", "status"] + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/orca/clmm/create-pool": { + "post": { + "tags": ["/connector/orca"], + "description": "Create and initialize a new Orca (Whirlpools) CLMM pool at an initial price. Does not open or seed a position.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will create and initialize the pool", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string", + "example": "USDC" + }, + "tickSpacing": { + "description": "Tick spacing (fee tier) for the new Whirlpool. A FeeTier account for this config+tickSpacing must already exist on-chain. Common Orca values: 1, 2, 8, 16, 64, 128, 256.", + "minimum": 1, + "type": "integer", + "example": 64 + }, + "initialPrice": { + "description": "Initial price as quote per base. If omitted, the current market price is fetched from the unified swap router so the pool opens on-market. No position is opened; only the pool is created.", + "type": "number", + "example": 200 + } + }, + "required": ["baseToken", "quoteToken", "tickSpacing"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was initialized at (quote per base)", + "type": "number" + }, + "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + }, + "required": ["signature", "status", "poolAddress"] } } } @@ -7462,9 +7928,9 @@ }, "walletAddress": { "description": "Solana wallet address that will open the position", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, "lowerPrice": { "description": "Lower price bound for the position", @@ -7514,31 +7980,16 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionAddress": { - "type": "string" - }, - "positionRent": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": [ "fee", @@ -7575,14 +8026,14 @@ }, "walletAddress": { "description": "Solana wallet address that will add liquidity", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, "positionAddress": { "description": "Position NFT address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" }, "baseTokenAmount": { "description": "Amount of base token to deposit", @@ -7617,25 +8068,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -7658,23 +8098,19 @@ "schema": { "type": "object", "properties": { - "network": { - "description": "Solana network to use", - "default": "mainnet-beta", - "type": "string" - }, + "network": { "description": "Solana network to use", "default": "mainnet-beta", "type": "string" }, "walletAddress": { "description": "Solana wallet address that will remove liquidity", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, "positionAddress": { "description": "Position NFT address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" }, - "liquidityPct": { + "percentageToRemove": { "minimum": 0, "maximum": 100, "description": "Percentage of liquidity to remove", @@ -7705,25 +8141,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -7739,7 +8164,7 @@ "/connectors/orca/clmm/collect-fees": { "post": { "tags": ["/connector/orca"], - "description": "Collect fees from an Orca position", + "description": "Collect fees and rewards from an Orca position", "requestBody": { "content": { "application/json": { @@ -7754,14 +8179,14 @@ }, "walletAddress": { "description": "Solana wallet address that will collect fees", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, "positionAddress": { "description": "Position NFT address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" } }, "required": ["positionAddress"] @@ -7778,25 +8203,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -7827,14 +8241,14 @@ }, "walletAddress": { "description": "Solana wallet address that will close the position", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, "positionAddress": { "description": "Position NFT address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" } }, "required": ["positionAddress"] @@ -7851,34 +8265,17 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionRentRefunded": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": [ "fee", @@ -7904,20 +8301,14 @@ "description": "Get AMM pool information from Raydium", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", "in": "query", "name": "poolAddress", @@ -7933,27 +8324,13 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - } + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } }, "required": [ "address", @@ -7977,20 +8354,14 @@ "description": "Get info about a Raydium AMM position", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", "in": "query", "name": "poolAddress", @@ -7998,10 +8369,7 @@ "description": "Raydium AMM pool address" }, { - "schema": { - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", - "type": "string" - }, + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "in": "query", "name": "walletAddress", "required": false, @@ -8016,29 +8384,32 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "walletAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "lpTokenAmount": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "price": { - "type": "number" + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "positionAddress": { + "description": "Address of the individual position (NFT position account)", + "type": "string" + }, + "lpTokenAmount": { + "description": "Liquidity held by this position (LP units)", + "type": "number" + }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] + } } }, "required": [ @@ -8064,20 +8435,14 @@ "description": "Quote amounts for a new Raydium AMM liquidity position", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", "in": "query", "name": "poolAddress", @@ -8085,9 +8450,7 @@ "description": "Raydium AMM pool address" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -8095,9 +8458,7 @@ "description": "Amount of base token to add" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -8105,12 +8466,7 @@ "description": "Amount of quote token to add" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, "example": 2, "in": "query", "name": "slippagePct", @@ -8126,21 +8482,11 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - } + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" } }, "required": [ "baseLimited", @@ -8156,16 +8502,7 @@ "500": { "description": "Default Response", "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "error": { - "type": "string" - } - } - } - } + "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } } } } @@ -8177,20 +8514,14 @@ "description": "Get swap quote for Raydium AMM", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", "in": "query", "name": "poolAddress", @@ -8198,9 +8529,7 @@ "description": "AMM pool address (optional - can be looked up from baseToken and quoteToken)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "SOL", "in": "query", "name": "baseToken", @@ -8208,9 +8537,7 @@ "description": "Token to determine swap direction" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -8218,9 +8545,7 @@ "description": "The other token in the pair (optional - required if poolAddress not provided)" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "amount", @@ -8228,23 +8553,14 @@ "description": "Amount to swap" }, { - "schema": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "in": "query", "name": "side", "required": true, "description": "Trade direction" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, "example": 2, "in": "query", "name": "slippagePct", @@ -8260,36 +8576,16 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ "poolAddress", @@ -8307,50 +8603,26 @@ } } } - } - }, - "/connectors/raydium/amm/execute-swap": { - "post": { - "tags": ["/connector/raydium"], - "description": "Execute a swap on Raydium AMM or CPMM", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "walletAddress": { - "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" - }, - "network": { - "type": "string", - "default": "mainnet-beta" - }, - "poolAddress": { - "type": "string", - "example": "" - }, - "baseToken": { - "type": "string", - "example": "SOL" - }, - "quoteToken": { - "type": "string", - "example": "USDC" - }, - "amount": { - "type": "number", - "example": 0.01 - }, - "side": { - "type": "string", - "example": "SELL" - }, - "slippagePct": { - "type": "number", - "example": 1 - } + } + }, + "/connectors/raydium/amm/execute-swap": { + "post": { + "tags": ["/connector/raydium"], + "description": "Execute a swap on Raydium AMM or CPMM", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletAddress": { "type": "string", "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, + "network": { "type": "string", "default": "mainnet-beta" }, + "poolAddress": { "type": "string", "example": "" }, + "baseToken": { "type": "string", "example": "SOL" }, + "quoteToken": { "type": "string", "example": "USDC" }, + "amount": { "type": "number", "example": 0.01 }, + "side": { "type": "string", "example": "SELL" }, + "slippagePct": { "type": "number", "example": 1 } }, "required": ["baseToken", "amount", "side"] } @@ -8366,37 +8638,18 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "fee": { - "type": "number" - }, - "baseTokenBalanceChange": { - "type": "number" - }, - "quoteTokenBalanceChange": { - "type": "number" - } + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } }, "required": [ "tokenIn", @@ -8435,7 +8688,7 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "poolAddress": { @@ -8448,11 +8701,7 @@ "type": "number", "example": 0.01 }, - "quoteTokenAmount": { - "description": "Amount of quote token to add", - "type": "number", - "example": 2 - }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -8476,25 +8725,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -8525,7 +8763,7 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "poolAddress": { @@ -8555,25 +8793,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -8586,31 +8813,129 @@ } } }, + "/connectors/raydium/amm/create-pool": { + "post": { + "tags": ["/connector/raydium"], + "description": "Create a new Raydium CPMM (CP-Swap) pool and seed it with initial liquidity", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will create and seed the pool", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string", + "example": "USDC" + }, + "baseTokenAmount": { + "description": "Amount of base token to seed the pool with", + "type": "number", + "example": 0.01 + }, + "quoteTokenAmount": { + "description": "Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. If omitted (and no initialPrice), the current market price is fetched from the swap router.", + "type": "number", + "example": 2 + }, + "initialPrice": { + "description": "Initial price as quote per base. Overrides quoteTokenAmount. If both are omitted, the pool is seeded at the current market price so it is not immediately arbitraged.", + "type": "number" + }, + "feeConfigIndex": { + "description": "Index into the CPMM fee-config list returned by the Raydium API (getCpmmConfigs). Default 0 selects the first/lowest fee tier.", + "default": 0, + "minimum": 0, + "type": "integer" + }, + "openTime": { + "description": "Unix timestamp (seconds) when trading opens. Default 0 opens the pool immediately on confirmation.", + "default": 0, + "minimum": 0, + "type": "integer" + } + }, + "required": ["baseToken", "quoteToken", "baseTokenAmount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was seeded at (quote per base)", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status", "poolAddress"] + } + } + } + } + } + } + }, "/connectors/raydium/clmm/pool-info": { "get": { "tags": ["/connector/raydium"], "description": "Get CLMM pool information from Raydium", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", "in": "query", "name": "poolAddress", "required": true, "description": "Raydium CLMM pool address" + }, + { + "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "in": "query", + "name": "binCount", + "required": false, + "description": "If > 0, include a `bins` array (per-tickSpacing token amounts around the current tick), mirroring Meteora pool-info.bins[]. Default 0 — pool-info skips the extra tick-array fetch." } ], "responses": { @@ -8621,32 +8946,27 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] + } } }, "required": [ @@ -8672,21 +8992,15 @@ "description": "Retrieve all positions owned by a user's wallet across all Raydium CLMM pools", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "schema": { "type": "string" }, + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", "required": true, @@ -8703,51 +9017,19 @@ "items": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -8778,31 +9060,22 @@ "description": "Get info about a Raydium CLMM position", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, - "example": "\u003Csample-position-address\u003E", + "schema": { "type": "string" }, + "example": "", "in": "query", "name": "positionAddress", "required": true, "description": "Position NFT address" }, { - "schema": { - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", - "type": "string" - }, + "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "in": "query", "name": "walletAddress", "required": false, @@ -8817,51 +9090,19 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -8891,20 +9132,14 @@ "description": "Quote amounts for a new Raydium CLMM position", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 100, "in": "query", "name": "lowerPrice", @@ -8912,9 +9147,7 @@ "description": "Lower price bound for the position" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 300, "in": "query", "name": "upperPrice", @@ -8922,9 +9155,7 @@ "description": "Upper price bound for the position" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", "in": "query", "name": "poolAddress", @@ -8932,9 +9163,7 @@ "description": "Raydium CLMM pool address" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -8942,9 +9171,7 @@ "description": "Amount of base token to deposit" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -8952,12 +9179,7 @@ "description": "Amount of quote token to deposit" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, "example": 2, "in": "query", "name": "slippagePct", @@ -8971,23 +9193,13 @@ "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - }, + "type": "object", + "properties": { + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, "liquidity": {} }, "required": [ @@ -9010,20 +9222,14 @@ "description": "Get swap quote for Raydium CLMM", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", "in": "query", "name": "poolAddress", @@ -9031,9 +9237,7 @@ "description": "CLMM pool address (optional - can be looked up from tokens)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "SOL", "in": "query", "name": "baseToken", @@ -9041,9 +9245,7 @@ "description": "Token to determine swap direction" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -9051,9 +9253,7 @@ "description": "The other token in the pair" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "amount", @@ -9061,23 +9261,14 @@ "description": "Amount to swap" }, { - "schema": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "in": "query", "name": "side", "required": true, "description": "Trade direction" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, "example": 2, "in": "query", "name": "slippagePct", @@ -9093,36 +9284,16 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ "poolAddress", @@ -9154,9 +9325,9 @@ "properties": { "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, "network": { "description": "Solana network to use", @@ -9169,21 +9340,9 @@ "type": "string", "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv" }, - "baseToken": { - "description": "Base token symbol or address", - "type": "string", - "example": "SOL" - }, - "quoteToken": { - "description": "Quote token symbol or address", - "type": "string", - "example": "USDC" - }, - "amount": { - "description": "Amount to swap", - "type": "number", - "example": 0.01 - }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], @@ -9214,37 +9373,18 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "fee": { - "type": "number" - }, - "baseTokenBalanceChange": { - "type": "number" - }, - "quoteTokenBalanceChange": { - "type": "number" - } + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } }, "required": [ "tokenIn", @@ -9283,7 +9423,7 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "lowerPrice": { @@ -9334,31 +9474,16 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionAddress": { - "type": "string" - }, - "positionRent": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": [ "fee", @@ -9377,6 +9502,80 @@ } } }, + "/connectors/raydium/clmm/create-pool": { + "post": { + "tags": ["/connector/raydium"], + "description": "Create and initialize a new Raydium CLMM pool at an initial price. Does not open or seed a position.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will create and initialize the pool", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string", + "example": "USDC" + }, + "initialPrice": { + "description": "Initial price as quote per base. If omitted, the current market price is fetched from the unified swap router so the pool opens on-market. No position is opened; only the pool is created.", + "type": "number", + "example": 200 + }, + "ammConfigIndex": { + "description": "Index into the CLMM amm-config list returned by the Raydium API (getClmmConfigs). Each config carries a fee tier and tickSpacing. Default 0 selects the first/lowest tier.", + "default": 0, + "minimum": 0, + "type": "integer" + } + }, + "required": ["baseToken", "quoteToken"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was initialized at (quote per base)", + "type": "number" + }, + "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + }, + "required": ["signature", "status", "poolAddress"] + } + } + } + } + } + } + }, "/connectors/raydium/clmm/add-liquidity": { "post": { "tags": ["/connector/raydium"], @@ -9395,24 +9594,20 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "positionAddress": { "description": "Position NFT address", "type": "string", - "example": "\u003Csample-position-address\u003E" + "example": "" }, "baseTokenAmount": { "description": "Amount of base token to add", "type": "number", "example": 0.01 }, - "quoteTokenAmount": { - "description": "Amount of quote token to add", - "type": "number", - "example": 2 - }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -9436,25 +9631,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -9485,7 +9669,7 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "positionAddress": { @@ -9515,25 +9699,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -9556,17 +9729,9 @@ "schema": { "type": "object", "properties": { - "network": { - "type": "string", - "default": "mainnet-beta" - }, - "walletAddress": { - "type": "string", - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn" - }, - "positionAddress": { - "type": "string" - } + "network": { "type": "string", "default": "mainnet-beta" }, + "walletAddress": { "type": "string", "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, + "positionAddress": { "type": "string" } }, "required": ["positionAddress"] } @@ -9582,25 +9747,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -9631,7 +9785,7 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "positionAddress": { @@ -9654,34 +9808,17 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionRentRefunded": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": [ "fee", @@ -9709,7 +9846,18 @@ { "schema": { "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "in": "query", @@ -9718,9 +9866,7 @@ "description": "The EVM network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", @@ -9728,9 +9874,7 @@ "description": "First token in the trading pair" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -9738,9 +9882,7 @@ "description": "Second token in the trading pair" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", @@ -9748,86 +9890,48 @@ "description": "Amount of base token to trade" }, { - "schema": { - "enum": ["BUY", "SELL"], - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "type": "string" }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", - "type": "string" - }, - "in": "query", - "name": "walletAddress", - "required": false, - "description": "Wallet address for more accurate quotes (optional)" - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "quoteId": { - "description": "Unique identifier for this quote", - "type": "string" - }, - "tokenIn": { - "description": "Address of the token being swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token being swapped to", - "type": "string" - }, - "amountIn": { - "description": "Amount of tokenIn to be swapped", - "type": "number" - }, - "amountOut": { - "description": "Expected amount of tokenOut to receive", - "type": "number" - }, - "price": { - "description": "Exchange rate between tokenIn and tokenOut", - "type": "number" - }, - "priceImpactPct": { - "description": "Estimated price impact percentage (0-100)", - "type": "number" - }, + "schema": { "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, + "in": "query", + "name": "walletAddress", + "required": false, + "description": "Wallet address for more accurate quotes (optional)" + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { - "description": "Maximum amount of tokenIn that will be spent", - "type": "number" - }, - "routePath": { - "description": "Human-readable route path", - "type": "string" - } + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "routePath": { "description": "Human-readable route path", "type": "string" } }, "required": [ "quoteId", @@ -9859,14 +9963,25 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, "network": { "description": "The blockchain network to use", "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "quoteId": { @@ -9889,10 +10004,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -9900,26 +10012,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -9960,14 +10057,25 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, "network": { "description": "The blockchain network to use", "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "baseToken": { @@ -9975,16 +10083,8 @@ "type": "string", "example": "WETH" }, - "quoteToken": { - "description": "The other token in the pair", - "type": "string", - "example": "USDC" - }, - "amount": { - "description": "Amount of base token to trade", - "type": "number", - "example": 0.001 - }, + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.001 }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -10013,10 +10113,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -10024,26 +10121,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -10080,7 +10162,18 @@ { "schema": { "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "in": "query", @@ -10089,9 +10182,7 @@ "description": "The EVM network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "0x88A43bbDF9D098eEC7bCEda4e2494615dfD9bB9C", "in": "query", "name": "poolAddress", @@ -10107,27 +10198,13 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - } + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } }, "required": [ "address", @@ -10150,51 +10227,17 @@ "tags": ["/connector/uniswap"], "description": "Get position information for a Uniswap V2 pool", "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { - "schema": { - "type": "string", - "default": "base" - }, - "in": "query", - "name": "network", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "schema": { "type": "string" }, + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "in": "query", "name": "walletAddress", "required": false }, - { - "schema": { - "type": "string" - }, - "example": "", - "in": "query", - "name": "poolAddress", - "required": true - }, - { - "schema": { - "type": "string" - }, - "example": "WETH", - "in": "query", - "name": "baseToken", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false - } + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } ], "responses": { "200": { @@ -10204,29 +10247,32 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "walletAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "lpTokenAmount": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "price": { - "type": "number" + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "positionAddress": { + "description": "Address of the individual position (NFT position account)", + "type": "string" + }, + "lpTokenAmount": { + "description": "Liquidity held by this position (LP units)", + "type": "number" + }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] + } } }, "required": [ @@ -10251,70 +10297,19 @@ "tags": ["/connector/uniswap"], "description": "Get swap quote for Uniswap V2 AMM", "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": false }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, { - "schema": { - "type": "string", - "default": "base" - }, - "in": "query", - "name": "network", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "", - "in": "query", - "name": "poolAddress", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "WETH", - "in": "query", - "name": "baseToken", - "required": true - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false - }, - { - "schema": { - "type": "number" - }, - "example": 0.001, - "in": "query", - "name": "amount", - "required": true - }, - { - "schema": { - "type": "string", - "enum": ["BUY", "SELL"] - }, + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, "example": "SELL", "in": "query", "name": "side", "required": true }, - { - "schema": { - "type": "number" - }, - "example": 1, - "in": "query", - "name": "slippagePct", - "required": false - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } ], "responses": { "200": { @@ -10324,36 +10319,16 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ "poolAddress", @@ -10364,83 +10339,39 @@ "price", "minAmountOut", "maxAmountIn", - "priceImpactPct" - ] - } - } - } - } - } - } - }, - "/connectors/uniswap/amm/quote-liquidity": { - "get": { - "tags": ["/connector/uniswap"], - "description": "Get liquidity quote for a Uniswap V2 pool", - "parameters": [ - { - "schema": { - "type": "string", - "default": "base" - }, - "in": "query", - "name": "network", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "", - "in": "query", - "name": "poolAddress", - "required": true - }, + "priceImpactPct" + ] + } + } + } + } + } + } + }, + "/connectors/uniswap/amm/quote-liquidity": { + "get": { + "tags": ["/connector/uniswap"], + "description": "Get liquidity quote for a Uniswap V2 pool", + "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "baseTokenAmount", "required": true }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 2.5, "in": "query", "name": "quoteTokenAmount", "required": true }, - { - "schema": { - "type": "number" - }, - "example": 1, - "in": "query", - "name": "slippagePct", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "WETH", - "in": "query", - "name": "baseToken", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } ], "responses": { "200": { @@ -10450,21 +10381,11 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - } + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" } }, "required": [ "baseLimited", @@ -10492,13 +10413,24 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, "network": { "description": "The EVM network to use", "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "poolAddress": { @@ -10506,26 +10438,10 @@ "default": "", "type": "string" }, - "baseToken": { - "description": "Base token symbol or address", - "type": "string", - "example": "WETH" - }, - "quoteToken": { - "description": "Quote token symbol or address", - "type": "string", - "example": "USDC" - }, - "amount": { - "description": "Amount to swap", - "type": "number", - "example": 0.001 - }, - "side": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "WETH" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.001 }, + "side": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -10548,10 +10464,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -10559,26 +10472,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -10620,24 +10518,119 @@ "network": { "description": "The EVM network to use", "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "walletAddress": { "description": "Wallet address that will add liquidity", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "poolAddress": { - "description": "Address of the Uniswap V2 pool", + "poolAddress": { "description": "Address of the Uniswap V2 pool", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number" + } + }, + "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/uniswap/amm/create-pool": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Create a new Uniswap V2 (AMM) pool and seed it with initial liquidity (fixed 0.30% fee)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The EVM network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, - "baseTokenAmount": { - "description": "Amount of base token to add", - "type": "number" + "walletAddress": { + "description": "Wallet address that will create and seed the pool", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" + }, + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", + "type": "string", + "example": "WETH" + }, + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string", + "example": "USDC" }, + "baseTokenAmount": { "description": "Amount of base token to seed the pool with", "type": "number" }, "quoteTokenAmount": { - "description": "Amount of quote token to add", + "description": "Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. If omitted (and no initialPrice), the current market price is fetched from the unified swap router.", + "type": "number" + }, + "initialPrice": { + "description": "Initial price as quote per base. Overrides quoteTokenAmount. If both are omitted, the pool is seeded at the current market price so it is not immediately arbitraged.", "type": "number" }, "slippagePct": { @@ -10646,18 +10639,9 @@ "description": "Maximum acceptable slippage percentage", "default": 2, "type": "number" - }, - "gasPrice": { - "description": "Gas price in wei for the transaction", - "type": "string" - }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 } }, - "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] + "required": ["baseToken", "quoteToken", "baseTokenAmount"] } } }, @@ -10671,30 +10655,24 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was seeded at (quote per base)", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } }, - "required": ["signature", "status"] + "required": ["signature", "status", "poolAddress"] } } } @@ -10715,32 +10693,31 @@ "network": { "description": "The EVM network to use", "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "walletAddress": { "description": "Wallet address that will remove liquidity", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", - "type": "string" - }, - "poolAddress": { - "description": "Address of the Uniswap V2 pool", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, + "poolAddress": { "description": "Address of the Uniswap V2 pool", "type": "string" }, "percentageToRemove": { "minimum": 0, "maximum": 100, "description": "Percentage of liquidity to remove", "type": "number" - }, - "gasPrice": { - "description": "Gas price in wei for the transaction", - "type": "string" - }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 } }, "required": ["poolAddress", "percentageToRemove"] @@ -10757,25 +10734,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -10796,7 +10762,18 @@ { "schema": { "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "in": "query", @@ -10805,50 +10782,50 @@ "description": "The EVM network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "0xd0b53d9277642d899df5c87a3966a349a798f224", "in": "query", "name": "poolAddress", "required": true, "description": "Uniswap V3 pool address" - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" + }, + { + "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "in": "query", + "name": "binCount", + "required": false, + "description": "If > 0, include a `bins` array (per-tickSpacing token amounts around the current tick), mirroring Meteora pool-info.bins[]. Default 0 — pool-info skips the extra eth_calls." + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] + } } }, "required": [ @@ -10873,19 +10850,9 @@ "tags": ["/connector/uniswap"], "description": "Get position information for a Uniswap V3 position", "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { - "schema": { - "type": "string", - "default": "base" - }, - "in": "query", - "name": "network", - "required": false - }, - { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "1234", "in": "query", "name": "positionAddress", @@ -10901,51 +10868,19 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -10975,20 +10910,15 @@ "description": "Get all Uniswap V3 positions owned by a wallet", "parameters": [ { - "schema": { - "default": "base", - "type": "string" - }, + "schema": { "default": "base", "type": "string" }, "example": "base", "in": "query", "name": "network", "required": false }, { - "schema": { - "type": "string" - }, - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "schema": { "type": "string" }, + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "in": "query", "name": "walletAddress", "required": true @@ -11004,51 +10934,19 @@ "items": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -11079,67 +10977,37 @@ "description": "Get a quote for opening a position on Uniswap V3", "parameters": [ { - "schema": { - "type": "string", - "default": "base" - }, + "schema": { "type": "string", "default": "base" }, "example": "base", "in": "query", "name": "network", "required": false }, + { "schema": { "type": "number" }, "example": 2000, "in": "query", "name": "lowerPrice", "required": true }, + { "schema": { "type": "number" }, "example": 4000, "in": "query", "name": "upperPrice", "required": true }, { - "schema": { - "type": "number" - }, - "example": 2000, - "in": "query", - "name": "lowerPrice", - "required": true - }, - { - "schema": { - "type": "number" - }, - "example": 4000, - "in": "query", - "name": "upperPrice", - "required": true - }, - { - "schema": { - "type": "string", - "default": "0xd0b53d9277642d899df5c87a3966a349a798f224" - }, + "schema": { "type": "string", "default": "0xd0b53d9277642d899df5c87a3966a349a798f224" }, "example": "0xd0b53d9277642d899df5c87a3966a349a798f224", "in": "query", "name": "poolAddress", "required": true }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "baseTokenAmount", "required": false }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 3, "in": "query", "name": "quoteTokenAmount", "required": false }, { - "schema": { - "minimum": 0, - "maximum": 100, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, "in": "query", "name": "slippagePct", "required": false @@ -11153,21 +11021,11 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - }, + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, "liquidity": {} }, "required": [ @@ -11189,70 +11047,25 @@ "tags": ["/connector/uniswap"], "description": "Get swap quote for Uniswap V3 CLMM", "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { - "schema": { - "type": "string", - "default": "base" - }, - "in": "query", - "name": "network", - "required": false - }, - { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "in": "query", "name": "poolAddress", "required": false, "description": "Pool address (optional - can be looked up from baseToken and quoteToken)" }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, { - "schema": { - "type": "string" - }, - "example": "WETH", - "in": "query", - "name": "baseToken", - "required": true - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false - }, - { - "schema": { - "type": "number" - }, - "example": 0.001, - "in": "query", - "name": "amount", - "required": true - }, - { - "schema": { - "type": "string", - "enum": ["BUY", "SELL"] - }, + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, "example": "SELL", "in": "query", "name": "side", "required": true }, - { - "schema": { - "type": "number" - }, - "example": 1, - "in": "query", - "name": "slippagePct", - "required": false - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } ], "responses": { "200": { @@ -11262,36 +11075,16 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ "poolAddress", @@ -11323,14 +11116,25 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, "network": { "description": "The blockchain network to use", "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "baseToken": { @@ -11338,16 +11142,8 @@ "type": "string", "example": "WETH" }, - "quoteToken": { - "description": "The other token in the pair", - "type": "string", - "example": "USDC" - }, - "amount": { - "description": "Amount of base token to trade", - "type": "number", - "example": 0.001 - }, + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.001 }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -11376,10 +11172,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -11387,43 +11180,89 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, - "baseTokenBalanceChange": { - "description": "Change in base token balance (negative for decrease)", - "type": "number" - }, - "quoteTokenBalanceChange": { - "description": "Change in quote token balance (negative for decrease)", - "type": "number" - } + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, + "baseTokenBalanceChange": { + "description": "Change in base token balance (negative for decrease)", + "type": "number" + }, + "quoteTokenBalanceChange": { + "description": "Change in quote token balance (negative for decrease)", + "type": "number" + } + }, + "required": [ + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "fee", + "baseTokenBalanceChange", + "quoteTokenBalanceChange" + ] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/uniswap/clmm/open-position": { + "post": { + "tags": ["/connector/uniswap"], + "description": "Open a new liquidity position in a Uniswap V3 pool", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, + "lowerPrice": { "type": "number", "example": 1000 }, + "upperPrice": { "type": "number", "example": 4000 }, + "poolAddress": { "type": "string", "example": "0xd0b53d9277642d899df5c87a3966a349a798f224" }, + "baseTokenAmount": { "type": "number", "example": 0.001 }, + "quoteTokenAmount": { "type": "number", "example": 3 }, + "slippagePct": { "type": "number", "example": 1 } + }, + "required": ["lowerPrice", "upperPrice", "poolAddress"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": [ - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", "fee", - "baseTokenBalanceChange", - "quoteTokenBalanceChange" + "positionAddress", + "positionRent", + "baseTokenAmountAdded", + "quoteTokenAmountAdded" ] } }, @@ -11435,10 +11274,10 @@ } } }, - "/connectors/uniswap/clmm/open-position": { + "/connectors/uniswap/clmm/create-pool": { "post": { "tags": ["/connector/uniswap"], - "description": "Open a new liquidity position in a Uniswap V3 pool", + "description": "Create and initialize a new Uniswap V3 (CLMM) pool at an initial price (no liquidity seeded)", "requestBody": { "content": { "application/json": { @@ -11446,39 +11285,49 @@ "type": "object", "properties": { "network": { - "type": "string", - "default": "base" + "description": "The EVM network to use", + "default": "mainnet", + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], + "type": "string" }, "walletAddress": { - "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" - }, - "lowerPrice": { - "type": "number", - "example": 1000 - }, - "upperPrice": { - "type": "number", - "example": 4000 + "description": "Wallet address that will create and initialize the pool", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" }, - "poolAddress": { + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", "type": "string", - "example": "0xd0b53d9277642d899df5c87a3966a349a798f224" + "example": "WETH" }, - "baseTokenAmount": { - "type": "number", - "example": 0.001 + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string", + "example": "USDC" }, - "quoteTokenAmount": { + "fee": { + "description": "Fee tier in hundredths of a bip: 100 (0.01%), 500 (0.05%), 3000 (0.30%), or 10000 (1.00%)", + "enum": [100, 500, 3000, 10000], "type": "number", - "example": 3 + "example": 3000 }, - "slippagePct": { - "type": "number", - "example": 1 + "initialPrice": { + "description": "Initial price as quote per base. If omitted, the current market price is fetched from the unified swap router so the pool opens on-market and is not immediately arbitraged.", + "type": "number" } }, - "required": ["lowerPrice", "upperPrice", "poolAddress"] + "required": ["baseToken", "quoteToken", "fee"] } } }, @@ -11492,42 +11341,16 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was initialized at (quote per base)", "type": "number" }, - "data": { - "type": "object", - "properties": { - "fee": { - "type": "number" - }, - "positionAddress": { - "type": "string" - }, - "positionRent": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } - }, - "required": [ - "fee", - "positionAddress", - "positionRent", - "baseTokenAmountAdded", - "quoteTokenAmountAdded" - ] - } + "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } }, - "required": ["signature", "status"] + "required": ["signature", "status", "poolAddress"] } } } @@ -11548,41 +11371,34 @@ "network": { "description": "The EVM network to use", "default": "mainnet", - "enum": ["arbitrum", "avalanche", "base", "bsc", "celo", "mainnet", "optimism", "polygon"], + "enum": [ + "arbitrum", + "avalanche", + "base", + "bsc", + "celo", + "mainnet", + "optimism", + "polygon", + "robinhoodchain", + "unichain" + ], "type": "string" }, "walletAddress": { "description": "Wallet address that will add liquidity", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", - "type": "string" - }, - "positionAddress": { - "description": "NFT token ID of the position", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "baseTokenAmount": { - "description": "Amount of base token to add", - "type": "number" - }, - "quoteTokenAmount": { - "description": "Amount of quote token to add", - "type": "number" - }, + "positionAddress": { "description": "NFT token ID of the position", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, "slippagePct": { "minimum": 0, "maximum": 100, "description": "Maximum acceptable slippage percentage", "default": 2, "type": "number" - }, - "gasPrice": { - "description": "Gas price in wei for the transaction", - "type": "string" - }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 } }, "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] @@ -11599,25 +11415,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -11640,25 +11445,10 @@ "schema": { "type": "object", "properties": { - "network": { - "type": "string", - "default": "base" - }, - "walletAddress": { - "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" - }, - "positionAddress": { - "type": "string", - "description": "Position NFT token ID", - "example": "1234" - }, - "percentageToRemove": { - "type": "number", - "minimum": 0, - "maximum": 100, - "example": 50 - } + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, + "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" }, + "percentageToRemove": { "type": "number", "minimum": 0, "maximum": 100, "example": 50 } }, "required": ["positionAddress", "percentageToRemove"] } @@ -11674,25 +11464,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -11715,19 +11494,9 @@ "schema": { "type": "object", "properties": { - "network": { - "type": "string", - "default": "base" - }, - "walletAddress": { - "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" - }, - "positionAddress": { - "type": "string", - "description": "Position NFT token ID", - "example": "1234" - } + "network": { "type": "string", "default": "base" }, + "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, + "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" } }, "required": ["positionAddress"] } @@ -11743,25 +11512,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -11784,15 +11542,9 @@ "schema": { "type": "object", "properties": { - "network": { - "type": "string" - }, - "walletAddress": { - "type": "string" - }, - "positionAddress": { - "type": "string" - } + "network": { "type": "string" }, + "walletAddress": { "type": "string" }, + "positionAddress": { "type": "string" } }, "required": ["positionAddress"] } @@ -11808,34 +11560,17 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionRentRefunded": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": [ "fee", @@ -11872,9 +11607,7 @@ "description": "The EVM network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", @@ -11882,9 +11615,7 @@ "description": "First token in the trading pair" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -11892,9 +11623,7 @@ "description": "Second token in the trading pair" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 1, "in": "query", "name": "amount", @@ -11902,21 +11631,14 @@ "description": "Amount of base token to trade" }, { - "schema": { - "enum": ["BUY", "SELL"], - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "type": "string" }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", @@ -11924,19 +11646,14 @@ "description": "Maximum acceptable slippage percentage" }, { - "schema": { - "default": true, - "type": "boolean" - }, + "schema": { "default": true, "type": "boolean" }, "in": "query", "name": "indicativePrice", "required": false, "description": "If true, returns indicative pricing only (no commitment). If false, returns firm quote ready for execution" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "in": "query", "name": "takerAddress", "required": false, @@ -11951,71 +11668,31 @@ "schema": { "type": "object", "properties": { - "quoteId": { - "description": "Unique identifier for this quote", - "type": "string" - }, - "tokenIn": { - "description": "Address of the token being swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token being swapped to", - "type": "string" - }, - "amountIn": { - "description": "Amount of tokenIn to be swapped", - "type": "number" - }, - "amountOut": { - "description": "Expected amount of tokenOut to receive", - "type": "number" - }, - "price": { - "description": "Exchange rate between tokenIn and tokenOut", - "type": "number" - }, - "priceImpactPct": { - "description": "Estimated price impact percentage (0-100)", - "type": "number" - }, + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { - "description": "Maximum amount of tokenIn that will be spent", - "type": "number" - }, + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, "expirationTime": { "description": "Unix timestamp when this quote expires (only for firm quotes)", "type": "number" }, - "gasEstimate": { - "description": "Estimated gas required for the swap", - "type": "string" - }, - "sources": { - "description": "Liquidity sources used for this quote", - "type": "array", - "items": {} - }, + "gasEstimate": { "description": "Estimated gas required for the swap", "type": "string" }, + "sources": { "description": "Liquidity sources used for this quote", "type": "array", "items": {} }, "allowanceTarget": { "description": "Contract address that needs token approval", "type": "string" }, - "to": { - "description": "Contract address to send transaction to", - "type": "string" - }, - "data": { - "description": "Encoded transaction data", - "type": "string" - }, - "value": { - "description": "ETH value to send with transaction", - "type": "string" - } + "to": { "description": "Contract address to send transaction to", "type": "string" }, + "data": { "description": "Encoded transaction data", "type": "string" }, + "value": { "description": "ETH value to send with transaction", "type": "string" } }, "required": [ "quoteId", @@ -12048,7 +11725,7 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, "network": { @@ -12062,15 +11739,6 @@ "description": "ID of the quote to execute", "type": "string", "example": "123e4567-e89b-12d3-a456-426614174000" - }, - "gasPrice": { - "description": "Gas price in wei for the transaction", - "type": "string" - }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 1000000 } }, "required": ["quoteId"] @@ -12086,11 +11754,8 @@ "application/json": { "schema": { "type": "object", - "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "properties": { + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -12098,26 +11763,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -12158,9 +11808,9 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, "network": { "description": "The blockchain network to use", @@ -12174,16 +11824,8 @@ "type": "string", "example": "WETH" }, - "quoteToken": { - "description": "The other token in the pair", - "type": "string", - "example": "USDC" - }, - "amount": { - "description": "Amount of base token to trade", - "type": "number", - "example": 1 - }, + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 1 }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -12195,15 +11837,6 @@ "description": "Maximum acceptable slippage percentage", "type": "number", "example": 1 - }, - "gasPrice": { - "description": "Gas price in wei for the transaction", - "type": "string" - }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 } }, "required": ["baseToken", "quoteToken", "amount", "side"] @@ -12220,10 +11853,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -12231,26 +11861,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -12285,20 +11900,14 @@ "description": "Get an executable swap quote from Pancakeswap Universal Router", "parameters": [ { - "schema": { - "default": "mainnet", - "enum": ["arbitrum", "base", "bsc", "mainnet"], - "type": "string" - }, + "schema": { "default": "mainnet", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "The EVM network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "USDT", "in": "query", "name": "baseToken", @@ -12306,9 +11915,7 @@ "description": "First token in the trading pair" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "WBNB", "in": "query", "name": "quoteToken", @@ -12316,9 +11923,7 @@ "description": "Second token in the trading pair" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 10, "in": "query", "name": "amount", @@ -12326,32 +11931,21 @@ "description": "Amount of base token to trade" }, { - "schema": { - "enum": ["BUY", "SELL"], - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "type": "string" }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", - "type": "string" - }, + "schema": { "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, "in": "query", "name": "walletAddress", "required": false, @@ -12366,46 +11960,19 @@ "schema": { "type": "object", "properties": { - "quoteId": { - "description": "Unique identifier for this quote", - "type": "string" - }, - "tokenIn": { - "description": "Address of the token being swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token being swapped to", - "type": "string" - }, - "amountIn": { - "description": "Amount of tokenIn to be swapped", - "type": "number" - }, - "amountOut": { - "description": "Expected amount of tokenOut to receive", - "type": "number" - }, - "price": { - "description": "Exchange rate between tokenIn and tokenOut", - "type": "number" - }, - "priceImpactPct": { - "description": "Estimated price impact percentage (0-100)", - "type": "number" - }, + "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, + "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, + "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, + "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, + "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { - "description": "Maximum amount of tokenIn that will be spent", - "type": "number" - }, - "routePath": { - "description": "Human-readable route path", - "type": "string" - } + "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "routePath": { "description": "Human-readable route path", "type": "string" } }, "required": [ "quoteId", @@ -12437,9 +12004,9 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, "network": { "description": "The blockchain network to use", @@ -12467,10 +12034,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -12478,26 +12042,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -12538,9 +12087,9 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, "network": { "description": "The blockchain network to use", @@ -12553,16 +12102,8 @@ "type": "string", "example": "USDT" }, - "quoteToken": { - "description": "The other token in the pair", - "type": "string", - "example": "WBNB" - }, - "amount": { - "description": "Amount of base token to trade", - "type": "number", - "example": 10 - }, + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "WBNB" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 10 }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -12591,10 +12132,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -12602,26 +12140,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -12656,20 +12179,14 @@ "description": "Get AMM pool information from Pancakeswap V2", "parameters": [ { - "schema": { - "default": "mainnet", - "enum": ["arbitrum", "base", "bsc", "mainnet"], - "type": "string" - }, + "schema": { "default": "mainnet", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "The EVM network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "0x88A43bbDF9D098eEC7bCEda4e2494615dfD9bB9C", "in": "query", "name": "poolAddress", @@ -12685,27 +12202,13 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - } + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } }, "required": [ "address", @@ -12728,51 +12231,17 @@ "tags": ["/connector/pancakeswap"], "description": "Get position information for a Pancakeswap V2 pool", "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { - "schema": { - "type": "string", - "default": "base" - }, - "in": "query", - "name": "network", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "schema": { "type": "string" }, + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "in": "query", "name": "walletAddress", "required": false }, - { - "schema": { - "type": "string" - }, - "example": "", - "in": "query", - "name": "poolAddress", - "required": true - }, - { - "schema": { - "type": "string" - }, - "example": "WETH", - "in": "query", - "name": "baseToken", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false - } + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } ], "responses": { "200": { @@ -12782,117 +12251,69 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "walletAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "lpTokenAmount": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "price": { - "type": "number" + "poolAddress": { "type": "string" }, + "walletAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "lpTokenAmount": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "price": { "type": "number" }, + "positions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "positionAddress": { + "description": "Address of the individual position (NFT position account)", + "type": "string" + }, + "lpTokenAmount": { + "description": "Liquidity held by this position (LP units)", + "type": "number" + }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] + } } }, - "required": [ - "poolAddress", - "walletAddress", - "baseTokenAddress", - "quoteTokenAddress", - "lpTokenAmount", - "baseTokenAmount", - "quoteTokenAmount", - "price" - ] - } - } - } - } - } - } - }, - "/connectors/pancakeswap/amm/quote-swap": { - "get": { - "tags": ["/connector/pancakeswap"], - "description": "Get swap quote for Pancakeswap V2 AMM", - "parameters": [ - { - "schema": { - "type": "string", - "default": "base" - }, - "in": "query", - "name": "network", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "", - "in": "query", - "name": "poolAddress", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "WETH", - "in": "query", - "name": "baseToken", - "required": true - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false - }, - { - "schema": { - "type": "number" - }, - "example": 0.001, - "in": "query", - "name": "amount", - "required": true - }, + "required": [ + "poolAddress", + "walletAddress", + "baseTokenAddress", + "quoteTokenAddress", + "lpTokenAmount", + "baseTokenAmount", + "quoteTokenAmount", + "price" + ] + } + } + } + } + } + } + }, + "/connectors/pancakeswap/amm/quote-swap": { + "get": { + "tags": ["/connector/pancakeswap"], + "description": "Get swap quote for Pancakeswap V2 AMM", + "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": false }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, { - "schema": { - "type": "string", - "enum": ["BUY", "SELL"] - }, + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, "example": "SELL", "in": "query", "name": "side", "required": true }, - { - "schema": { - "type": "number" - }, - "example": 1, - "in": "query", - "name": "slippagePct", - "required": false - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } ], "responses": { "200": { @@ -12902,36 +12323,16 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ "poolAddress", @@ -12956,69 +12357,25 @@ "tags": ["/connector/pancakeswap"], "description": "Get liquidity quote for a Pancakeswap V2 pool", "parameters": [ + { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, + { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, { - "schema": { - "type": "string", - "default": "base" - }, - "in": "query", - "name": "network", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "", - "in": "query", - "name": "poolAddress", - "required": true - }, - { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "baseTokenAmount", "required": true }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 2.5, "in": "query", "name": "quoteTokenAmount", "required": true }, - { - "schema": { - "type": "number" - }, - "example": 1, - "in": "query", - "name": "slippagePct", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "WETH", - "in": "query", - "name": "baseToken", - "required": false - }, - { - "schema": { - "type": "string" - }, - "example": "USDC", - "in": "query", - "name": "quoteToken", - "required": false - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false }, + { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, + { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } ], "responses": { "200": { @@ -13028,21 +12385,11 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - } + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" } }, "required": [ "baseLimited", @@ -13070,7 +12417,7 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, "network": { @@ -13084,26 +12431,10 @@ "default": "", "type": "string" }, - "baseToken": { - "description": "Base token symbol or address", - "type": "string", - "example": "USDT" - }, - "quoteToken": { - "description": "Quote token symbol or address", - "type": "string", - "example": "WBNB" - }, - "amount": { - "description": "Amount to swap", - "type": "number", - "example": 10 - }, - "side": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "USDT" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "WBNB" }, + "amount": { "description": "Amount to swap", "type": "number", "example": 10 }, + "side": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -13126,10 +12457,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -13137,26 +12465,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -13203,36 +12516,18 @@ }, "walletAddress": { "description": "Wallet address that will add liquidity", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", - "type": "string" - }, - "poolAddress": { - "description": "Address of the Pancakeswap V2 pool", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "baseTokenAmount": { - "description": "Amount of base token to add", - "type": "number" - }, - "quoteTokenAmount": { - "description": "Amount of quote token to add", - "type": "number" - }, + "poolAddress": { "description": "Address of the Pancakeswap V2 pool", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, "slippagePct": { "minimum": 0, "maximum": 100, "description": "Maximum acceptable slippage percentage", "default": 2, "type": "number" - }, - "gasPrice": { - "description": "Gas price in wei for the transaction", - "type": "string" - }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 } }, "required": ["poolAddress", "baseTokenAmount", "quoteTokenAmount"] @@ -13249,25 +12544,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -13298,30 +12582,100 @@ }, "walletAddress": { "description": "Wallet address that will remove liquidity", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", - "type": "string" - }, - "poolAddress": { - "description": "Address of the Pancakeswap V2 pool", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, + "poolAddress": { "description": "Address of the Pancakeswap V2 pool", "type": "string" }, "percentageToRemove": { "minimum": 0, "maximum": 100, "description": "Percentage of liquidity to remove", "type": "number" + } + }, + "required": ["poolAddress", "percentageToRemove"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "data": { + "type": "object", + "properties": { + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } + }, + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + } + }, + "required": ["signature", "status"] + } + } + } + } + } + } + }, + "/connectors/pancakeswap/amm/create-pool": { + "post": { + "tags": ["/connector/pancakeswap"], + "description": "Create a new Pancakeswap V2 (AMM) pool and seed it with initial liquidity (fixed 0.25% fee)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The EVM network to use", + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string" }, - "gasPrice": { - "description": "Gas price in wei for the transaction", + "walletAddress": { + "description": "Wallet address that will create and seed the pool", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", + "type": "string", + "example": "USDT" + }, + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string", + "example": "WBNB" + }, + "baseTokenAmount": { "description": "Amount of base token to seed the pool with", "type": "number" }, + "quoteTokenAmount": { + "description": "Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. If omitted (and no initialPrice), the current market price is fetched from the unified swap router.", + "type": "number" + }, + "initialPrice": { + "description": "Initial price as quote per base. Overrides quoteTokenAmount. If both are omitted, the pool is seeded at the current market price so it is not immediately arbitraged.", + "type": "number" + }, + "slippagePct": { + "minimum": 0, + "maximum": 100, + "description": "Maximum acceptable slippage percentage", + "default": 2, + "type": "number" } }, - "required": ["poolAddress", "percentageToRemove"] + "required": ["baseToken", "quoteToken", "baseTokenAmount"] } } }, @@ -13335,30 +12689,24 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was seeded at (quote per base)", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, - "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } }, - "required": ["signature", "status"] + "required": ["signature", "status", "poolAddress"] } } } @@ -13372,11 +12720,7 @@ "description": "Get CLMM pool information from Pancakeswap V3", "parameters": [ { - "schema": { - "default": "bsc", - "enum": ["arbitrum", "base", "bsc", "mainnet"], - "type": "string" - }, + "schema": { "default": "bsc", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, "example": "bsc", "in": "query", "name": "network", @@ -13384,14 +12728,19 @@ "description": "The EVM network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "0x172fcd41e0913e95784454622d1c3724f546f849", "in": "query", "name": "poolAddress", "required": true, "description": "Pancakeswap V3 pool address" + }, + { + "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "in": "query", + "name": "binCount", + "required": false, + "description": "If > 0, include a `bins` array (per-tickSpacing token amounts around the current tick), mirroring Meteora pool-info.bins[]. Default 0 — pool-info skips the extra eth_calls." } ], "responses": { @@ -13402,32 +12751,27 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] + } } }, "required": [ @@ -13453,19 +12797,14 @@ "description": "Get position information for a Pancakeswap V3 position", "parameters": [ { - "schema": { - "type": "string", - "default": "bsc" - }, + "schema": { "type": "string", "default": "bsc" }, "example": "bsc", "in": "query", "name": "network", "required": false }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "1234", "in": "query", "name": "positionAddress", @@ -13481,51 +12820,19 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -13555,20 +12862,15 @@ "description": "Get all Pancakeswap V3 positions owned by a wallet", "parameters": [ { - "schema": { - "default": "bsc", - "type": "string" - }, + "schema": { "default": "bsc", "type": "string" }, "example": "bsc", "in": "query", "name": "network", "required": false }, { - "schema": { - "type": "string" - }, - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "schema": { "type": "string" }, + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "in": "query", "name": "walletAddress", "required": true @@ -13584,51 +12886,19 @@ "items": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -13659,67 +12929,37 @@ "description": "Get a quote for opening a position on Pancakeswap V3", "parameters": [ { - "schema": { - "type": "string", - "default": "bsc" - }, + "schema": { "type": "string", "default": "bsc" }, "example": "bsc", "in": "query", "name": "network", "required": false }, + { "schema": { "type": "number" }, "example": 0.0008, "in": "query", "name": "lowerPrice", "required": true }, + { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "upperPrice", "required": true }, { - "schema": { - "type": "number" - }, - "example": 0.0008, - "in": "query", - "name": "lowerPrice", - "required": true - }, - { - "schema": { - "type": "number" - }, - "example": 0.001, - "in": "query", - "name": "upperPrice", - "required": true - }, - { - "schema": { - "type": "string", - "default": "0x172fcd41e0913e95784454622d1c3724f546f849" - }, + "schema": { "type": "string", "default": "0x172fcd41e0913e95784454622d1c3724f546f849" }, "example": "0x172fcd41e0913e95784454622d1c3724f546f849", "in": "query", "name": "poolAddress", "required": true }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 10, "in": "query", "name": "baseTokenAmount", "required": false }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "quoteTokenAmount", "required": false }, { - "schema": { - "minimum": 0, - "maximum": 100, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "type": "number" }, "in": "query", "name": "slippagePct", "required": false @@ -13733,21 +12973,11 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - }, + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, "liquidity": {} }, "required": [ @@ -13770,70 +13000,30 @@ "description": "Get swap quote for Pancakeswap V3 CLMM", "parameters": [ { - "schema": { - "type": "string", - "default": "bsc" - }, + "schema": { "type": "string", "default": "bsc" }, "example": "bsc", "in": "query", "name": "network", "required": false }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "in": "query", "name": "poolAddress", "required": false, "description": "Pool address (optional - can be looked up from baseToken and quoteToken)" }, + { "schema": { "type": "string" }, "example": "USDT", "in": "query", "name": "baseToken", "required": true }, + { "schema": { "type": "string" }, "example": "WBNB", "in": "query", "name": "quoteToken", "required": false }, + { "schema": { "type": "number" }, "example": 10, "in": "query", "name": "amount", "required": true }, { - "schema": { - "type": "string" - }, - "example": "USDT", - "in": "query", - "name": "baseToken", - "required": true - }, - { - "schema": { - "type": "string" - }, - "example": "WBNB", - "in": "query", - "name": "quoteToken", - "required": false - }, - { - "schema": { - "type": "number" - }, - "example": 10, - "in": "query", - "name": "amount", - "required": true - }, - { - "schema": { - "type": "string", - "enum": ["BUY", "SELL"] - }, + "schema": { "type": "string", "enum": ["BUY", "SELL"] }, "example": "SELL", "in": "query", "name": "side", "required": true }, - { - "schema": { - "type": "number" - }, - "example": 1, - "in": "query", - "name": "slippagePct", - "required": false - } + { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } ], "responses": { "200": { @@ -13843,36 +13033,16 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ "poolAddress", @@ -13904,9 +13074,9 @@ "properties": { "walletAddress": { "description": "Wallet address that will execute the swap", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, "network": { "description": "The blockchain network to use", @@ -13919,16 +13089,8 @@ "type": "string", "example": "USDT" }, - "quoteToken": { - "description": "The other token in the pair", - "type": "string", - "example": "WBNB" - }, - "amount": { - "description": "Amount of base token to trade", - "type": "number", - "example": 10 - }, + "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "WBNB" }, + "amount": { "description": "Amount of base token to trade", "type": "number", "example": 10 }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -13957,10 +13119,7 @@ "schema": { "type": "object", "properties": { - "signature": { - "description": "Transaction signature/hash", - "type": "string" - }, + "signature": { "description": "Transaction signature/hash", "type": "string" }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -13968,26 +13127,11 @@ "data": { "type": "object", "properties": { - "tokenIn": { - "description": "Address of the token swapped from", - "type": "string" - }, - "tokenOut": { - "description": "Address of the token swapped to", - "type": "string" - }, - "amountIn": { - "description": "Actual amount of tokenIn swapped", - "type": "number" - }, - "amountOut": { - "description": "Actual amount of tokenOut received", - "type": "number" - }, - "fee": { - "description": "Transaction fee paid", - "type": "number" - }, + "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, + "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, + "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, + "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, + "fee": { "description": "Transaction fee paid", "type": "number" }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -14026,40 +13170,18 @@ "schema": { "type": "object", "properties": { - "network": { - "type": "string", - "default": "bsc", - "example": "bsc" - }, - "walletAddress": { - "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" - }, - "lowerPrice": { - "type": "number", - "example": 0.0008 - }, - "upperPrice": { - "type": "number", - "example": 0.001 - }, + "network": { "type": "string", "default": "bsc", "example": "bsc" }, + "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, + "lowerPrice": { "type": "number", "example": 0.0008 }, + "upperPrice": { "type": "number", "example": 0.001 }, "poolAddress": { "type": "string", "default": "0x172fcd41e0913e95784454622d1c3724f546f849", "example": "0x172fcd41e0913e95784454622d1c3724f546f849" }, - "baseTokenAmount": { - "type": "number", - "example": 10 - }, - "quoteTokenAmount": { - "type": "number", - "example": 0.01 - }, - "slippagePct": { - "type": "number", - "example": 1 - } + "baseTokenAmount": { "type": "number", "example": 10 }, + "quoteTokenAmount": { "type": "number", "example": 0.01 }, + "slippagePct": { "type": "number", "example": 1 } }, "required": ["lowerPrice", "upperPrice", "poolAddress"] } @@ -14075,31 +13197,16 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionAddress": { - "type": "string" - }, - "positionRent": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": [ "fee", @@ -14132,41 +13239,23 @@ "description": "The EVM network to use", "default": "bsc", "enum": ["arbitrum", "base", "bsc", "mainnet"], - "type": "string", - "example": "bsc" - }, - "walletAddress": { - "description": "Wallet address that will add liquidity", - "default": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50", - "type": "string" - }, - "positionAddress": { - "description": "NFT token ID of the position", - "type": "string" - }, - "baseTokenAmount": { - "description": "Amount of base token to add", - "type": "number" + "type": "string", + "example": "bsc" }, - "quoteTokenAmount": { - "description": "Amount of quote token to add", - "type": "number" + "walletAddress": { + "description": "Wallet address that will add liquidity", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" }, + "positionAddress": { "description": "NFT token ID of the position", "type": "string" }, + "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, "slippagePct": { "minimum": 0, "maximum": 100, "description": "Maximum acceptable slippage percentage", "default": 2, "type": "number" - }, - "gasPrice": { - "description": "Gas price in wei for the transaction", - "type": "string" - }, - "maxGas": { - "description": "Maximum gas limit for the transaction", - "type": "number", - "example": 300000 } }, "required": ["positionAddress", "baseTokenAmount", "quoteTokenAmount"] @@ -14183,25 +13272,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -14224,26 +13302,10 @@ "schema": { "type": "object", "properties": { - "network": { - "type": "string", - "default": "bsc", - "example": "bsc" - }, - "walletAddress": { - "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" - }, - "positionAddress": { - "type": "string", - "description": "Position NFT token ID", - "example": "1234" - }, - "percentageToRemove": { - "type": "number", - "minimum": 0, - "maximum": 100, - "example": 50 - } + "network": { "type": "string", "default": "bsc", "example": "bsc" }, + "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, + "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" }, + "percentageToRemove": { "type": "number", "minimum": 0, "maximum": 100, "example": 50 } }, "required": ["positionAddress", "percentageToRemove"] } @@ -14259,25 +13321,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -14300,20 +13351,9 @@ "schema": { "type": "object", "properties": { - "network": { - "type": "string", - "default": "bsc", - "example": "bsc" - }, - "walletAddress": { - "type": "string", - "example": "0xB6B3140Eb3953BCE564f937948f98Ab5A8286a50" - }, - "positionAddress": { - "type": "string", - "description": "Position NFT token ID", - "example": "1234" - } + "network": { "type": "string", "default": "bsc", "example": "bsc" }, + "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, + "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" } }, "required": ["positionAddress"] } @@ -14329,25 +13369,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -14370,15 +13399,9 @@ "schema": { "type": "object", "properties": { - "network": { - "type": "string" - }, - "walletAddress": { - "type": "string" - }, - "positionAddress": { - "type": "string" - } + "network": { "type": "string" }, + "walletAddress": { "type": "string" }, + "positionAddress": { "type": "string" } }, "required": ["positionAddress"] } @@ -14394,34 +13417,17 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionRentRefunded": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": [ "fee", @@ -14441,31 +13447,106 @@ } } }, + "/connectors/pancakeswap/clmm/create-pool": { + "post": { + "tags": ["/connector/pancakeswap"], + "description": "Create and initialize a new Pancakeswap V3 (CLMM) pool at an initial price (no liquidity seeded)", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "The EVM network to use", + "default": "bsc", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string", + "example": "bsc" + }, + "walletAddress": { + "description": "Wallet address that will create and initialize the pool", + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" + }, + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", + "type": "string", + "example": "USDT" + }, + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string", + "example": "WBNB" + }, + "fee": { + "description": "Pancakeswap V3 fee tier in hundredths of a bip: 100 (0.01%), 500 (0.05%), 2500 (0.25%), or 10000 (1.00%)", + "enum": [100, 500, 2500, 10000], + "type": "number", + "example": 2500 + }, + "initialPrice": { + "description": "Initial price as quote per base. If omitted, the current market price is fetched from the unified swap router so the pool opens on-market and is not immediately arbitraged.", + "type": "number" + } + }, + "required": ["baseToken", "quoteToken", "fee"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was initialized at (quote per base)", + "type": "number" + }, + "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + }, + "required": ["signature", "status", "poolAddress"] + } + } + } + } + } + } + }, "/connectors/pancakeswap-sol/clmm/pool-info": { "get": { "tags": ["/connector/pancakeswap-sol"], "description": "Get CLMM pool information from PancakeSwap Solana", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", "name": "poolAddress", "required": true, "description": "PancakeSwap CLMM pool address" + }, + { + "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "in": "query", + "name": "binCount", + "required": false, + "description": "If > 0, include a `bins` array of per-tick liquidity around the active tick. Default 0 = skip the bin fetch." } ], "responses": { @@ -14476,32 +13557,27 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "binStep": { - "type": "number" - }, - "feePct": { - "type": "number" - }, - "price": { - "type": "number" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "activeBinId": { - "type": "number" + "address": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "binStep": { "type": "number" }, + "feePct": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "activeBinId": { "type": "number" }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { "type": "number" }, + "price": { "type": "number" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] + } } }, "required": [ @@ -14527,20 +13603,14 @@ "description": "Get CLMM position information from PancakeSwap Solana", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "", "in": "query", "name": "positionAddress", @@ -14556,51 +13626,19 @@ "schema": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -14630,30 +13668,22 @@ "description": "Retrieve all positions owned by a user's wallet across all PancakeSwap Solana CLMM pools", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, - "example": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "schema": { "type": "string" }, + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", "required": true, "description": "Solana wallet address to check for positions" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", "name": "poolAddress", @@ -14671,51 +13701,19 @@ "items": { "type": "object", "properties": { - "address": { - "type": "string" - }, - "poolAddress": { - "type": "string" - }, - "baseTokenAddress": { - "type": "string" - }, - "quoteTokenAddress": { - "type": "string" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseFeeAmount": { - "type": "number" - }, - "quoteFeeAmount": { - "type": "number" - }, - "lowerBinId": { - "type": "number" - }, - "upperBinId": { - "type": "number" - }, - "lowerPrice": { - "type": "number" - }, - "upperPrice": { - "type": "number" - }, - "price": { - "type": "number" - }, - "rewardTokenAddress": { - "type": "string" - }, - "rewardAmount": { - "type": "number" - } + "address": { "type": "string" }, + "poolAddress": { "type": "string" }, + "baseTokenAddress": { "type": "string" }, + "quoteTokenAddress": { "type": "string" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseFeeAmount": { "type": "number" }, + "quoteFeeAmount": { "type": "number" }, + "lowerBinId": { "type": "number" }, + "upperBinId": { "type": "number" }, + "lowerPrice": { "type": "number" }, + "upperPrice": { "type": "number" }, + "price": { "type": "number" } }, "required": [ "address", @@ -14744,22 +13742,16 @@ "get": { "tags": ["/connector/pancakeswap-sol"], "description": "Quote position amounts for PancakeSwap Solana CLMM (simplified)", - "parameters": [ - { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "parameters": [ + { + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", "name": "poolAddress", @@ -14767,9 +13759,7 @@ "description": "PancakeSwap CLMM pool address" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 150, "in": "query", "name": "lowerPrice", @@ -14777,9 +13767,7 @@ "description": "Lower price bound for the position" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 250, "in": "query", "name": "upperPrice", @@ -14787,9 +13775,7 @@ "description": "Upper price bound for the position" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -14797,9 +13783,7 @@ "description": "Amount of base token to deposit" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -14807,12 +13791,7 @@ "description": "Amount of quote token to deposit" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, "example": 2, "in": "query", "name": "slippagePct", @@ -14828,21 +13807,11 @@ "schema": { "type": "object", "properties": { - "baseLimited": { - "type": "boolean" - }, - "baseTokenAmount": { - "type": "number" - }, - "quoteTokenAmount": { - "type": "number" - }, - "baseTokenAmountMax": { - "type": "number" - }, - "quoteTokenAmountMax": { - "type": "number" - }, + "baseLimited": { "type": "boolean" }, + "baseTokenAmount": { "type": "number" }, + "quoteTokenAmount": { "type": "number" }, + "baseTokenAmountMax": { "type": "number" }, + "quoteTokenAmountMax": { "type": "number" }, "liquidity": {} }, "required": [ @@ -14865,20 +13834,14 @@ "description": "Get swap quote for PancakeSwap Solana CLMM with fee and estimated price impact based on pool liquidity", "parameters": [ { - "schema": { - "default": "mainnet-beta", - "enum": ["devnet", "mainnet-beta"], - "type": "string" - }, + "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", "name": "poolAddress", @@ -14886,9 +13849,7 @@ "description": "CLMM pool address (optional - can be looked up from tokens)" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "SOL", "in": "query", "name": "baseToken", @@ -14896,9 +13857,7 @@ "description": "Base token symbol or address" }, { - "schema": { - "type": "string" - }, + "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -14906,9 +13865,7 @@ "description": "Quote token symbol or address" }, { - "schema": { - "type": "number" - }, + "schema": { "type": "number" }, "example": 0.01, "in": "query", "name": "amount", @@ -14916,23 +13873,14 @@ "description": "Amount to swap" }, { - "schema": { - "enum": ["BUY", "SELL"], - "default": "SELL", - "type": "string" - }, + "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, "in": "query", "name": "side", "required": true, "description": "Trade direction" }, { - "schema": { - "minimum": 0, - "maximum": 100, - "default": 2, - "type": "number" - }, + "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, "example": 2, "in": "query", "name": "slippagePct", @@ -14948,36 +13896,16 @@ "schema": { "type": "object", "properties": { - "poolAddress": { - "type": "string" - }, - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "price": { - "type": "number" - }, - "slippagePct": { - "type": "number" - }, - "minAmountOut": { - "type": "number" - }, - "maxAmountIn": { - "type": "number" - }, - "priceImpactPct": { - "type": "number" - } + "poolAddress": { "type": "string" }, + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "price": { "type": "number" }, + "slippagePct": { "type": "number" }, + "minAmountOut": { "type": "number" }, + "maxAmountIn": { "type": "number" }, + "priceImpactPct": { "type": "number" } }, "required": [ "poolAddress", @@ -15015,7 +13943,7 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "poolAddress": { @@ -15023,21 +13951,9 @@ "type": "string", "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN" }, - "baseToken": { - "description": "Base token symbol or address", - "type": "string", - "example": "SOL" - }, - "quoteToken": { - "description": "Quote token symbol or address", - "type": "string", - "example": "USDC" - }, - "amount": { - "description": "Amount to swap", - "type": "number", - "example": 0.01 - }, + "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, + "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], @@ -15068,37 +13984,18 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "tokenIn": { - "type": "string" - }, - "tokenOut": { - "type": "string" - }, - "amountIn": { - "type": "number" - }, - "amountOut": { - "type": "number" - }, - "fee": { - "type": "number" - }, - "baseTokenBalanceChange": { - "type": "number" - }, - "quoteTokenBalanceChange": { - "type": "number" - } + "tokenIn": { "type": "string" }, + "tokenOut": { "type": "string" }, + "amountIn": { "type": "number" }, + "amountOut": { "type": "number" }, + "fee": { "type": "number" }, + "baseTokenBalanceChange": { "type": "number" }, + "quoteTokenBalanceChange": { "type": "number" } }, "required": [ "tokenIn", @@ -15119,6 +14016,81 @@ } } }, + "/connectors/pancakeswap-sol/clmm/create-pool": { + "post": { + "tags": ["/connector/pancakeswap-sol"], + "description": "Create and initialize a new PancakeSwap Solana CLMM pool at an initial price. Does not open or seed a position.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, + "walletAddress": { + "description": "Solana wallet address that will create and initialize the pool", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, + "baseToken": { + "description": "Base token symbol or address (becomes the pool base)", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "Quote token symbol or address (becomes the pool quote)", + "type": "string", + "example": "USDC" + }, + "initialPrice": { + "description": "Initial price as quote per base. If omitted, the current market price is fetched from the unified swap router so the pool opens on-market. No position is opened; only the pool is created.", + "type": "number", + "example": 200 + }, + "ammConfigIndex": { + "description": "Fee-config index; resolves to the amm_config PDA ([\"amm_config\", index]) and is validated on-chain. Each index is a fee tier created by the program admin. Default 0.", + "default": 0, + "minimum": 0, + "maximum": 65535, + "type": "integer" + } + }, + "required": ["baseToken", "quoteToken"] + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, + "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "price": { + "description": "Initial price the pool was initialized at (quote per base)", + "type": "number" + }, + "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + }, + "required": ["signature", "status", "poolAddress"] + } + } + } + } + } + } + }, "/connectors/pancakeswap-sol/clmm/open-position": { "post": { "tags": ["/connector/pancakeswap-sol"], @@ -15137,7 +14109,7 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "poolAddress": { @@ -15188,31 +14160,16 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionAddress": { - "type": "string" - }, - "positionRent": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "positionAddress": { "type": "string" }, + "positionRent": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": [ "fee", @@ -15249,24 +14206,16 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "positionAddress": { - "description": "Position NFT address", - "type": "string", - "example": "" - }, + "positionAddress": { "description": "Position NFT address", "type": "string", "example": "" }, "baseTokenAmount": { "description": "Amount of base token to add", "type": "number", "example": 0.01 }, - "quoteTokenAmount": { - "description": "Amount of quote token to add", - "type": "number", - "example": 2 - }, + "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -15290,25 +14239,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountAdded": { - "type": "number" - }, - "quoteTokenAmountAdded": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountAdded": { "type": "number" }, + "quoteTokenAmountAdded": { "type": "number" } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -15339,7 +14277,7 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, "positionAddress": { @@ -15369,25 +14307,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - } + "fee": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -15418,14 +14345,10 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "positionAddress": { - "description": "Position NFT address", - "type": "string", - "example": "" - } + "positionAddress": { "description": "Position NFT address", "type": "string", "example": "" } }, "required": ["positionAddress"] } @@ -15441,25 +14364,14 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -15490,14 +14402,10 @@ }, "walletAddress": { "description": "Solana wallet address", - "default": "Fxu7UU5D9ry1qDQj41Yp3oTeAdqxShzVYna44JE7E1gn", + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "positionAddress": { - "description": "Position NFT address to close", - "type": "string", - "example": "" - } + "positionAddress": { "description": "Position NFT address to close", "type": "string", "example": "" } }, "required": ["positionAddress"] } @@ -15513,34 +14421,17 @@ "schema": { "type": "object", "properties": { - "signature": { - "type": "string" - }, - "status": { - "description": "TransactionStatus enum value", - "type": "number" - }, + "signature": { "type": "string" }, + "status": { "description": "TransactionStatus enum value", "type": "number" }, "data": { "type": "object", "properties": { - "fee": { - "type": "number" - }, - "positionRentRefunded": { - "type": "number" - }, - "baseTokenAmountRemoved": { - "type": "number" - }, - "quoteTokenAmountRemoved": { - "type": "number" - }, - "baseFeeAmountCollected": { - "type": "number" - }, - "quoteFeeAmountCollected": { - "type": "number" - } + "fee": { "type": "number" }, + "positionRentRefunded": { "type": "number" }, + "baseTokenAmountRemoved": { "type": "number" }, + "quoteTokenAmountRemoved": { "type": "number" }, + "baseFeeAmountCollected": { "type": "number" }, + "quoteFeeAmountCollected": { "type": "number" } }, "required": [ "fee", @@ -15561,75 +14452,27 @@ } } }, - "servers": [ - { - "url": "http://localhost:15888" - } - ], + "servers": [{ "url": "http://localhost:15889" }], "tags": [ - { - "name": "/config", - "description": "System configuration endpoints" - }, - { - "name": "/wallet", - "description": "Wallet management endpoints" - }, - { - "name": "/tokens", - "description": "Token management endpoints" - }, - { - "name": "/pools", - "description": "Pool management endpoints" - }, - { - "name": "/trading/swap", - "description": "Unified cross-chain swap endpoints" - }, - { - "name": "/trading/clmm", - "description": "Unified cross-chain CLMM (Concentrated Liquidity) endpoints" - }, - { - "name": "/chain/solana", - "description": "Solana and SVM-based chain endpoints" - }, - { - "name": "/chain/ethereum", - "description": "Ethereum and EVM-based chain endpoints" - }, - { - "name": "/connector/jupiter", - "description": "Jupiter connector endpoints" - }, - { - "name": "/connector/meteora", - "description": "Meteora connector endpoints" - }, - { - "name": "/connector/orca", - "description": "Orca connector endpoints" - }, - { - "name": "/connector/raydium", - "description": "Raydium connector endpoints" - }, - { - "name": "/connector/uniswap", - "description": "Uniswap connector endpoints" - }, - { - "name": "/connector/0x", - "description": "0x connector endpoints" - }, - { - "name": "/connector/pancakeswap-sol", - "description": "PancakeSwap Solana connector endpoints" - }, - { - "name": "/connector/pancakeswap", - "description": "PancakeSwap EVM connector endpoints" - } + { "name": "/config", "description": "System configuration endpoints" }, + { "name": "/wallet", "description": "Wallet management endpoints" }, + { "name": "/tokens", "description": "Token management endpoints" }, + { "name": "/pools", "description": "Pool management endpoints" }, + { "name": "/trading/swap", "description": "Unified cross-chain swap endpoints" }, + { "name": "/trading/clmm", "description": "Unified cross-chain CLMM (Concentrated Liquidity) endpoints" }, + { "name": "/trading/amm", "description": "Unified cross-connector AMM endpoints (pool creation)" }, + { "name": "/chain/solana", "description": "Solana and SVM-based chain endpoints" }, + { "name": "/chain/ethereum", "description": "Ethereum and EVM-based chain endpoints" }, + { "name": "/connector/jupiter", "description": "Jupiter connector endpoints" }, + { "name": "/connector/meteora", "description": "Meteora connector endpoints" }, + { "name": "/connector/orca", "description": "Orca connector endpoints" }, + { "name": "/connector/raydium", "description": "Raydium connector endpoints" }, + { "name": "/connector/uniswap", "description": "Uniswap connector endpoints" }, + { "name": "/connector/0x", "description": "0x connector endpoints" }, + { "name": "/connector/pancakeswap-sol", "description": "PancakeSwap Solana connector endpoints" }, + { "name": "/connector/pancakeswap", "description": "PancakeSwap EVM connector endpoints" }, + { "name": "/connector/dflow", "description": "DFlow connector endpoints" }, + { "name": "/connector/okx", "description": "OKX DEX aggregator connector endpoints" }, + { "name": "/connector/titan", "description": "Titan connector endpoints" } ] } From 1fbbacf48124298590811966daedfd5438a96437 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 18:38:56 -0700 Subject: [PATCH 23/31] test(jupiter): assert removed fields' absence independently 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- test/connectors/jupiter/swap.test.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/connectors/jupiter/swap.test.js b/test/connectors/jupiter/swap.test.js index 3aa36c249d..602fbb852f 100644 --- a/test/connectors/jupiter/swap.test.js +++ b/test/connectors/jupiter/swap.test.js @@ -369,13 +369,11 @@ describe('Jupiter Swap Tests (Solana Mainnet)', () => { approximateIfNoExactOut: false, }), ); - expect(axios.post).toHaveBeenCalledWith( - `http://localhost:15888/connectors/${CONNECTOR}/execute-swap`, - expect.not.objectContaining({ - priorityLevel: expect.anything(), - maxLamports: expect.anything(), - }), - ); + // Assert each removed field's absence separately: a combined + // not.objectContaining({a, b}) passes when EITHER key is missing. + const executeBody = axios.post.mock.calls[axios.post.mock.calls.length - 1][1]; + expect(executeBody.priorityLevel).toBeUndefined(); + expect(executeBody.maxLamports).toBeUndefined(); }); test('returns pending swap execution', async () => { From da3008b7d1b62903389dfa2df3569bd9e8b891b3 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 22:30:30 -0700 Subject: [PATCH 24/31] refactor(trading): fold AMM pool-scoped swaps into the unified swap route /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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/trading/swap/execute.ts | 3 + src/trading/swap/quote.ts | 3 + .../trading-amm-routes/execute-swap.ts | 72 ------------------- src/trading/trading-amm-routes/index.ts | 2 - src/trading/trading-amm-routes/quote-swap.ts | 69 ------------------ src/trading/trading.routes.ts | 4 -- 6 files changed, 6 insertions(+), 147 deletions(-) delete mode 100644 src/trading/trading-amm-routes/execute-swap.ts delete mode 100644 src/trading/trading-amm-routes/quote-swap.ts diff --git a/src/trading/swap/execute.ts b/src/trading/swap/execute.ts index bd803b378c..855f466887 100644 --- a/src/trading/swap/execute.ts +++ b/src/trading/swap/execute.ts @@ -7,6 +7,7 @@ import { getSolanaNetworkConfig } from '../../chains/solana/solana.config'; import { executeSwap as zeroXRouterExecuteSwap } from '../../connectors/0x/router-routes/executeSwap'; import { executeSwap as dflowRouterExecuteSwap } from '../../connectors/dflow/router-routes/executeSwap'; import { executeSwap as jupiterRouterExecuteSwap } from '../../connectors/jupiter/router-routes/executeSwap'; +import { executeSwap as meteoraAmmExecuteSwap } from '../../connectors/meteora/amm-routes/executeSwap'; import { executeSwap as meteoraClmmExecuteSwap } from '../../connectors/meteora/clmm-routes/executeSwap'; import { executeSwap as okxRouterExecuteSwap } from '../../connectors/okx/router-routes/executeSwap'; import { executeSwap as orcaClmmExecuteSwap } from '../../connectors/orca/clmm-routes/executeSwap'; @@ -164,6 +165,8 @@ async function executeSolanaSwap( slippagePct, approximateIfNoExactOut, ); + } else if (providerKey === 'meteora/amm') { + return await meteoraAmmExecuteSwap(network, walletAddress, poolAddress!, baseToken, side, amount, slippagePct); } else if (providerKey === 'raydium/amm') { return await raydiumAmmExecuteSwap(network, walletAddress, poolAddress!, baseToken, side, amount, slippagePct); } else if (providerKey === 'raydium/clmm') { diff --git a/src/trading/swap/quote.ts b/src/trading/swap/quote.ts index ab26f50427..f284657ce0 100644 --- a/src/trading/swap/quote.ts +++ b/src/trading/swap/quote.ts @@ -7,6 +7,7 @@ import { getSolanaNetworkConfig } from '../../chains/solana/solana.config'; import { quoteSwap as zeroXRouterQuoteSwap } from '../../connectors/0x/router-routes/quoteSwap'; import { quoteSwap as dflowRouterQuoteSwap } from '../../connectors/dflow/router-routes/quoteSwap'; import { quoteSwap as jupiterRouterQuoteSwap } from '../../connectors/jupiter/router-routes/quoteSwap'; +import { quoteSwap as meteoraAmmQuoteSwap } from '../../connectors/meteora/amm-routes/quoteSwap'; import { quoteSwap as meteoraClmmQuoteSwap } from '../../connectors/meteora/clmm-routes/quoteSwap'; import { quoteSwap as okxRouterQuoteSwap } from '../../connectors/okx/router-routes/quoteSwap'; import { quoteSwap as orcaClmmQuoteSwap } from '../../connectors/orca/clmm-routes/quoteSwap'; @@ -155,6 +156,8 @@ async function getSolanaQuoteSwap( slippagePct, approximateIfNoExactOut, ); + } else if (providerKey === 'meteora/amm') { + return await meteoraAmmQuoteSwap(network, poolAddress!, baseToken, side, amount, slippagePct); } else if (providerKey === 'raydium/amm') { return await raydiumAmmQuoteSwap(network, poolAddress!, baseToken, side, amount, slippagePct); } else if (providerKey === 'raydium/clmm') { diff --git a/src/trading/trading-amm-routes/execute-swap.ts b/src/trading/trading-amm-routes/execute-swap.ts deleted file mode 100644 index d0000b9adc..0000000000 --- a/src/trading/trading-amm-routes/execute-swap.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Type, Static } from '@sinclair/typebox'; -import { FastifyPluginAsync } from 'fastify'; - -import { executeSwap as meteoraExecuteSwap } from '../../connectors/meteora/amm-routes/executeSwap'; -import { executeSwap as pancakeswapExecuteSwap } from '../../connectors/pancakeswap/amm-routes/executeSwap'; -import { executeSwap as raydiumExecuteSwap } from '../../connectors/raydium/amm-routes/executeSwap'; -import { executeSwap as uniswapExecuteSwap } from '../../connectors/uniswap/amm-routes/executeSwap'; -import { ExecuteSwapResponse, ExecuteSwapResponseType } from '../../schemas/amm-schema'; -import { httpErrors } from '../../services/error-handler'; -import { - AMM_CONNECTORS, - chainNetworkField, - connectorField, - defaultWallet, - parseChainNetwork, - rethrowRouteError, - slippagePctField, -} from '../common'; - -const UnifiedAmmExecuteSwapRequest = Type.Object({ - connector: connectorField(AMM_CONNECTORS, 'AMM connector'), - chainNetwork: chainNetworkField(), - walletAddress: Type.String({ description: 'Wallet address', default: defaultWallet }), - poolAddress: Type.String({ description: 'Pool contract address' }), - baseToken: Type.String({ description: 'Base token symbol or address (determines swap direction)' }), - amount: Type.Number({ description: 'Amount denominated in the base token' }), - side: Type.String({ description: 'Trade direction', enum: ['BUY', 'SELL'] }), - slippagePct: slippagePctField(), -}); - -export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { - fastify.post<{ - Body: Static; - Reply: ExecuteSwapResponseType; - }>( - '/execute-swap', - { - schema: { - description: 'Execute a swap against a specific AMM pool from any supported connector', - tags: ['/trading/amm'], - body: UnifiedAmmExecuteSwapRequest, - response: { 200: ExecuteSwapResponse }, - }, - }, - async (request) => { - try { - const { connector, chainNetwork, walletAddress, poolAddress, baseToken, amount, side, slippagePct } = - request.body; - const { network } = parseChainNetwork(chainNetwork); - const s = side as 'BUY' | 'SELL'; - switch (connector) { - case 'meteora': - return await meteoraExecuteSwap(network, walletAddress, poolAddress, baseToken, s, amount, slippagePct); - case 'raydium': - return await raydiumExecuteSwap(network, walletAddress, poolAddress, baseToken, s, amount, slippagePct); - case 'uniswap': - return await uniswapExecuteSwap(network, walletAddress, poolAddress, baseToken, s, amount, slippagePct); - case 'pancakeswap': - return await pancakeswapExecuteSwap(network, walletAddress, poolAddress, baseToken, s, amount, slippagePct); - default: - throw httpErrors.badRequest( - `Unsupported AMM connector: ${connector}. Supported: ${AMM_CONNECTORS.join(', ')}`, - ); - } - } catch (e: any) { - rethrowRouteError(e, 'Failed to execute AMM swap'); - } - }, - ); -}; - -export default executeSwapRoute; diff --git a/src/trading/trading-amm-routes/index.ts b/src/trading/trading-amm-routes/index.ts index 9b621f1548..46b40d7bec 100644 --- a/src/trading/trading-amm-routes/index.ts +++ b/src/trading/trading-amm-routes/index.ts @@ -2,8 +2,6 @@ export { createPoolRoute } from './create-pool'; export { poolInfoRoute } from './pool-info'; export { positionInfoRoute } from './position-info'; export { positionsOwnedRoute } from './positions-owned'; -export { quoteSwapRoute } from './quote-swap'; -export { executeSwapRoute } from './execute-swap'; export { quoteLiquidityRoute } from './quote-liquidity'; export { addLiquidityRoute } from './add-liquidity'; export { removeLiquidityRoute } from './remove-liquidity'; diff --git a/src/trading/trading-amm-routes/quote-swap.ts b/src/trading/trading-amm-routes/quote-swap.ts deleted file mode 100644 index bcafd5f38b..0000000000 --- a/src/trading/trading-amm-routes/quote-swap.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Type, Static } from '@sinclair/typebox'; -import { FastifyPluginAsync } from 'fastify'; - -import { quoteSwap as meteoraQuoteSwap } from '../../connectors/meteora/amm-routes/quoteSwap'; -import { quoteSwap as pancakeswapQuoteSwap } from '../../connectors/pancakeswap/amm-routes/quoteSwap'; -import { quoteSwap as raydiumQuoteSwap } from '../../connectors/raydium/amm-routes/quoteSwap'; -import { quoteSwap as uniswapQuoteSwap } from '../../connectors/uniswap/amm-routes/quoteSwap'; -import { QuoteSwapResponse, QuoteSwapResponseType } from '../../schemas/amm-schema'; -import { httpErrors } from '../../services/error-handler'; -import { - AMM_CONNECTORS, - chainNetworkField, - connectorField, - parseChainNetwork, - rethrowRouteError, - slippagePctField, -} from '../common'; - -const UnifiedAmmQuoteSwapRequest = Type.Object({ - connector: connectorField(AMM_CONNECTORS, 'AMM connector'), - chainNetwork: chainNetworkField(), - poolAddress: Type.String({ description: 'Pool contract address' }), - baseToken: Type.String({ description: 'Base token symbol or address (determines swap direction)' }), - amount: Type.Number({ description: 'Amount denominated in the base token' }), - side: Type.String({ description: 'Trade direction', enum: ['BUY', 'SELL'] }), - slippagePct: slippagePctField(), -}); - -export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { - fastify.get<{ - Querystring: Static; - Reply: QuoteSwapResponseType; - }>( - '/quote-swap', - { - schema: { - description: 'Get a swap quote against a specific AMM pool from any supported connector', - tags: ['/trading/amm'], - querystring: UnifiedAmmQuoteSwapRequest, - response: { 200: QuoteSwapResponse }, - }, - }, - async (request) => { - try { - const { connector, chainNetwork, poolAddress, baseToken, amount, side, slippagePct } = request.query; - const { network } = parseChainNetwork(chainNetwork); - const s = side as 'BUY' | 'SELL'; - switch (connector) { - case 'meteora': - return await meteoraQuoteSwap(network, poolAddress, baseToken, s, amount, slippagePct); - case 'raydium': - return await raydiumQuoteSwap(network, poolAddress, baseToken, s, amount, slippagePct); - case 'uniswap': - return await uniswapQuoteSwap(network, poolAddress, baseToken, s, amount, slippagePct); - case 'pancakeswap': - return await pancakeswapQuoteSwap(network, poolAddress, baseToken, s, amount, slippagePct); - default: - throw httpErrors.badRequest( - `Unsupported AMM connector: ${connector}. Supported: ${AMM_CONNECTORS.join(', ')}`, - ); - } - } catch (e: any) { - rethrowRouteError(e, 'Failed to get AMM swap quote'); - } - }, - ); -}; - -export default quoteSwapRoute; diff --git a/src/trading/trading.routes.ts b/src/trading/trading.routes.ts index 414d2132cf..fc0ba5a6c4 100644 --- a/src/trading/trading.routes.ts +++ b/src/trading/trading.routes.ts @@ -12,8 +12,6 @@ import { poolInfoRoute as ammPoolInfoRoute, positionInfoRoute as ammPositionInfoRoute, positionsOwnedRoute as ammPositionsOwnedRoute, - quoteSwapRoute as ammQuoteSwapRoute, - executeSwapRoute as ammExecuteSwapRoute, quoteLiquidityRoute as ammQuoteLiquidityRoute, addLiquidityRoute as ammAddLiquidityRoute, removeLiquidityRoute as ammRemoveLiquidityRoute, @@ -60,11 +58,9 @@ export const tradingAmmRoutes: FastifyPluginAsync = async (fastify) => { fastify.register(ammPoolInfoRoute); fastify.register(ammPositionInfoRoute); fastify.register(ammPositionsOwnedRoute); - fastify.register(ammQuoteSwapRoute); fastify.register(ammQuoteLiquidityRoute); // Register AMM transaction routes (unified cross-connector) - fastify.register(ammExecuteSwapRoute); fastify.register(ammAddLiquidityRoute); fastify.register(ammRemoveLiquidityRoute); fastify.register(createPoolRoute); From 828159e637edccb5f9d6e162c88b95ac6dc3cd90 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 22:55:27 -0700 Subject: [PATCH 25/31] fix(trading): fail loudly on landed-but-failed Solana txs; echo applied slippagePct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/chains/ethereum/ethereum.ts | 3 + src/chains/solana/solana.ts | 103 ++++++++----- .../0x/router-routes/executeQuote.ts | 3 + .../dflow/router-routes/executeQuote.ts | 12 +- .../jupiter/router-routes/executeQuote.ts | 10 +- .../meteora/amm-routes/addLiquidity.ts | 7 +- .../meteora/amm-routes/createPool.ts | 7 +- .../meteora/amm-routes/executeSwap.ts | 9 +- .../meteora/amm-routes/removeLiquidity.ts | 7 +- .../meteora/clmm-routes/addLiquidity.ts | 7 +- .../meteora/clmm-routes/closePosition.ts | 7 +- .../meteora/clmm-routes/collectFees.ts | 7 +- .../meteora/clmm-routes/createPool.ts | 7 +- .../meteora/clmm-routes/executeSwap.ts | 10 +- .../meteora/clmm-routes/openPosition.ts | 7 +- .../meteora/clmm-routes/removeLiquidity.ts | 7 +- .../okx/router-routes/executeQuote.ts | 10 +- .../orca/clmm-routes/closePosition.ts | 7 +- src/connectors/orca/clmm-routes/createPool.ts | 7 +- .../orca/clmm-routes/executeSwap.ts | 1 + .../orca/clmm-routes/openPosition.ts | 7 +- .../clmm-routes/addLiquidity.ts | 4 + .../clmm-routes/closePosition.ts | 4 + .../clmm-routes/collectFees.ts | 108 ++++++++++--- .../pancakeswap-sol/clmm-routes/createPool.ts | 4 + .../clmm-routes/executeSwap.ts | 6 +- .../clmm-routes/openPosition.ts | 4 + .../clmm-routes/removeLiquidity.ts | 4 + .../pancakeswap/amm-routes/executeSwap.ts | 1 + .../pancakeswap/clmm-routes/executeSwap.ts | 1 + .../pancakeswap/router-routes/executeQuote.ts | 4 +- .../raydium/amm-routes/addLiquidity.ts | 7 +- .../raydium/amm-routes/createPool.ts | 7 +- .../raydium/amm-routes/executeSwap.ts | 9 +- .../raydium/amm-routes/removeLiquidity.ts | 7 +- .../raydium/clmm-routes/addLiquidity.ts | 7 +- .../raydium/clmm-routes/closePosition.ts | 7 +- .../raydium/clmm-routes/createPool.ts | 7 +- .../raydium/clmm-routes/executeSwap.ts | 9 +- .../raydium/clmm-routes/openPosition.ts | 7 +- .../raydium/clmm-routes/removeLiquidity.ts | 7 +- .../titan/router-routes/executeQuote.ts | 12 +- .../uniswap/amm-routes/executeSwap.ts | 1 + .../uniswap/clmm-routes/executeSwap.ts | 1 + .../uniswap/router-routes/executeQuote.ts | 3 +- src/schemas/amm-schema.ts | 3 + src/schemas/chain-schema.ts | 5 + src/schemas/clmm-schema.ts | 3 + src/schemas/router-schema.ts | 5 + src/services/error-handler.ts | 6 + src/services/quote-cache.ts | 14 ++ .../solana/confirmation-helpers.test.ts | 134 ++++++++++++++++ ...sendAndConfirmTransactionForWallet.test.ts | 32 +++- .../dflow/router-routes/executeQuote.test.ts | 1 + .../meteora/clmm-routes/execute-swap.test.ts | 60 ++++++++ .../okx/router-routes/executeQuote.test.ts | 19 ++- .../orca/clmm-routes/closePosition.test.ts | 1 + .../orca/clmm-routes/create-pool.test.ts | 1 + .../orca/clmm-routes/openPosition.test.ts | 1 + .../clmm-routes/collectFees.test.ts | 143 ++++++++++++++++++ .../raydium/amm-routes/addLiquidity.test.ts | 1 + .../raydium/clmm-routes/openPosition.test.ts | 6 +- .../titan/router-routes/executeQuote.test.ts | 1 + 63 files changed, 718 insertions(+), 194 deletions(-) create mode 100644 test/chains/solana/confirmation-helpers.test.ts create mode 100644 test/connectors/pancakeswap-sol/clmm-routes/collectFees.test.ts diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index ec3e34a931..9e2d8a92df 100644 --- a/src/chains/ethereum/ethereum.ts +++ b/src/chains/ethereum/ethereum.ts @@ -1045,6 +1045,7 @@ export class Ethereum { expectedAmountOut: number, side?: 'BUY' | 'SELL', txHash?: string, // Optional tx hash for pending transactions + slippagePct?: number, // Slippage tolerance actually applied to the swap (echoed in data) ): { signature: string; status: number; @@ -1056,6 +1057,7 @@ export class Ethereum { fee: number; baseTokenBalanceChange: number; quoteTokenBalanceChange: number; + slippagePct?: number; }; } { if (!txReceipt) { @@ -1118,6 +1120,7 @@ export class Ethereum { fee, baseTokenBalanceChange, quoteTokenBalanceChange, + slippagePct, }, }; } diff --git a/src/chains/solana/solana.ts b/src/chains/solana/solana.ts index 6001dcc7d9..88c95a4a3c 100644 --- a/src/chains/solana/solana.ts +++ b/src/chains/solana/solana.ts @@ -1447,28 +1447,54 @@ export class Solana { } /** - * If a broadcast transaction landed on-chain but failed, throw the parsed program error; - * return silently when the transaction is missing or succeeded so the caller can apply - * its own confirmation handling. The confirmation helpers report a landed-and-failed - * transaction as unconfirmed, which callers would otherwise misreport as a timeout. + * If a broadcast transaction landed on-chain but failed, throw the parsed program error + * (400 TRANSACTION_FAILED — fees were paid, this is terminal, not retryable); return + * silently when the transaction is missing or succeeded so the caller can apply its own + * confirmation handling. The confirmation helpers report a landed-and-failed transaction + * as unconfirmed, which callers would otherwise misreport as a timeout or as PENDING. + * + * Pass `txData` when it is already at hand to skip the re-fetch; when the caller only + * has a null/absent txData the transaction is looked up once more, because the send + * helpers can report a landed-and-failed transaction with no data attached. */ - private async throwIfLandedWithError(signature: string): Promise { + public async throwIfLandedWithError(signature: string, txData?: any): Promise { if (!signature) return; - let txData: any = null; - try { - txData = await this.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); - } catch { - return; + if (!txData) { + try { + txData = await this.connection.getTransaction(signature, { + commitment: 'confirmed', + maxSupportedTransactionVersion: 0, + }); + } catch { + return; + } } if (!txData?.meta?.err) return; - const { simulationFailed } = await import('../../services/error-handler'); + throw await this.buildLandedWithErrorException(signature, txData); + } + + /** Build the shared landed-but-failed error from a transaction's on-chain data. */ + private async buildLandedWithErrorException(signature: string, txData: any): Promise { + const { transactionFailed } = await import('../../services/error-handler'); const { parseSolanaError } = await import('./solana-error-parser'); const logs: string[] = txData.meta.logMessages ?? []; const parsed = parseSolanaError([JSON.stringify(txData.meta.err), ...logs].join('\n')); - throw simulationFailed(`Transaction ${signature} landed on-chain but failed: ${parsed.message}`); + return transactionFailed(`Transaction ${signature} landed on-chain but failed: ${parsed.message}`); + } + + /** + * Route-level re-fetch of a just-sent transaction. Uses the retrying fetch (data can lag + * RPC visibility right after confirmation, so a single getTransaction call misreports a + * confirmed transaction as PENDING) and throws the shared landed-but-failed error when + * the transaction landed with an error. Returns null only when the transaction is + * genuinely not visible on-chain yet. + */ + public async getConfirmedTransactionData(signature: string): Promise { + const txData = await this._fetchTransactionWithRetry(signature); + if (txData?.meta?.err != null) { + throw await this.buildLandedWithErrorException(signature, txData); + } + return txData; } public async sendAndConfirmTransactionForWallet( @@ -2251,24 +2277,28 @@ export class Solana { /** * Helper function to handle transaction confirmation results - * Returns appropriate response object based on confirmation status + * Returns appropriate response object based on the transaction's on-chain data * @param signature Transaction signature - * @param confirmed Whether transaction was confirmed - * @param txData Transaction data (if available) + * @param txData Transaction data from the route-level re-fetch (pass null when the + * caller has none — the helper re-fetches with retry so a just-confirmed transaction + * whose data lags RPC visibility is not misreported as PENDING) * @param tokenIn Input token address * @param tokenOut Output token address * @param walletAddress Wallet address for balance changes * @param side Trade side (optional, for AMM/CLMM swaps) - * @returns Response object with status and data + * @param slippagePct Slippage tolerance actually applied to the swap (echoed in data) + * @returns Response object with status and data; throws the shared landed-but-failed + * error when the transaction landed on-chain with an error — existence of txData is + * never treated as confirmation by itself */ public async handleConfirmation( signature: string, - confirmed: boolean, txData: any, tokenIn: string, tokenOut: string, walletAddress: string, side?: 'BUY' | 'SELL', + slippagePct?: number, ): Promise<{ signature: string; status: number; @@ -2280,9 +2310,20 @@ export class Solana { fee: number; baseTokenBalanceChange: number; quoteTokenBalanceChange: number; + slippagePct?: number; }; }> { - if (confirmed && txData) { + if (!txData) { + txData = await this._fetchTransactionWithRetry(signature); + } + + // Defense: a landed-but-failed transaction must throw — never report it as + // confirmed (its data exists) or as pending. + if (txData?.meta?.err != null) { + throw await this.buildLandedWithErrorException(signature, txData); + } + + if (txData) { // Transaction confirmed, extract balance changes const { balanceChanges, fee } = await this.extractBalanceChangesAndFee(signature, walletAddress, [ tokenIn, @@ -2321,25 +2362,7 @@ export class Solana { fee, baseTokenBalanceChange: baseTokenBalanceChange!, quoteTokenBalanceChange: quoteTokenBalanceChange!, - }, - }; - } else if (txData && !confirmed) { - // Transaction exists but not confirmed - extract fee from txData - const fee = this.getFee(txData); - - logger.warn(`Transaction ${signature} not confirmed. May need higher priority fee.`); - - return { - signature, - status: -1, // NOT_CONFIRMED - data: { - tokenIn, - tokenOut, - amountIn: 0, - amountOut: 0, - fee, - baseTokenBalanceChange: 0, - quoteTokenBalanceChange: 0, + slippagePct, }, }; } else { diff --git a/src/connectors/0x/router-routes/executeQuote.ts b/src/connectors/0x/router-routes/executeQuote.ts index d1d2ac72b3..0318597212 100644 --- a/src/connectors/0x/router-routes/executeQuote.ts +++ b/src/connectors/0x/router-routes/executeQuote.ts @@ -75,6 +75,9 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str quote.buyTokenAddress, expectedAmountIn, expectedAmountOut, + undefined, + undefined, + quoteCache.getRequest(quoteId)?.slippagePct, ); // Handle different transaction states diff --git a/src/connectors/dflow/router-routes/executeQuote.ts b/src/connectors/dflow/router-routes/executeQuote.ts index 8cfba7e664..71b23c6b2f 100644 --- a/src/connectors/dflow/router-routes/executeQuote.ts +++ b/src/connectors/dflow/router-routes/executeQuote.ts @@ -22,7 +22,7 @@ export async function executeQuote( const solana = await Solana.getInstance(network); const dflow = await DFlow.getInstance(network); - const { inputToken, outputToken, quoteResponse } = cached; + const { inputToken, outputToken, quoteResponse, slippagePct } = cached; // Build the swap UNSIGNED with the wallet as authority, then sign + send via the // wallet-type-aware chokepoint (local keypair / Ledger) — no per-wallet-type branching here. @@ -32,18 +32,18 @@ export async function executeQuote( const transaction = await dflow.buildSwapTransactionUnsigned(walletAddress, quoteResponse); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Re-fetch with retry; a landed-but-failed transaction throws instead of being + // misreported as confirmed or pending. + const txData = await solana.getConfirmedTransactionData(signature); const result = await solana.handleConfirmation( signature, - txData !== null, txData, inputToken.address, outputToken.address, walletAddress, + undefined, + slippagePct, ); // Remove quote from cache only after successful execution (confirmed) diff --git a/src/connectors/jupiter/router-routes/executeQuote.ts b/src/connectors/jupiter/router-routes/executeQuote.ts index 5dd3b2d34d..dcd03103fa 100644 --- a/src/connectors/jupiter/router-routes/executeQuote.ts +++ b/src/connectors/jupiter/router-routes/executeQuote.ts @@ -39,19 +39,19 @@ export async function executeQuote( const transaction = await jupiter.buildSwapTransactionForHardwareWallet(walletAddress, quote); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Re-fetch with retry; a landed-but-failed transaction throws instead of being + // misreported as confirmed or pending. + const txData = await solana.getConfirmedTransactionData(signature); // Handle confirmation status const result = await solana.handleConfirmation( signature, - txData !== null, txData, inputToken.address, outputToken.address, walletAddress, + undefined, + quote.slippageBps != null ? quote.slippageBps / 100 : undefined, ); // Remove quote from cache only after successful execution (confirmed) diff --git a/src/connectors/meteora/amm-routes/addLiquidity.ts b/src/connectors/meteora/amm-routes/addLiquidity.ts index 4decdb7f4d..1b6a8c03bc 100644 --- a/src/connectors/meteora/amm-routes/addLiquidity.ts +++ b/src/connectors/meteora/amm-routes/addLiquidity.ts @@ -84,10 +84,9 @@ export async function addLiquidity( } const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress, extraSigners); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); if (txData) { const { balanceChanges } = await solana.extractBalanceChangesAndFee(signature, walletAddress, [ diff --git a/src/connectors/meteora/amm-routes/createPool.ts b/src/connectors/meteora/amm-routes/createPool.ts index abe36ca9bf..8f66a9c08f 100644 --- a/src/connectors/meteora/amm-routes/createPool.ts +++ b/src/connectors/meteora/amm-routes/createPool.ts @@ -196,10 +196,9 @@ export async function createPool( }); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress, [positionNft]); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); if (txData) { const { balanceChanges } = await solana.extractBalanceChangesAndFee(signature, walletAddress, [ diff --git a/src/connectors/meteora/amm-routes/executeSwap.ts b/src/connectors/meteora/amm-routes/executeSwap.ts index 8e12fe4031..e4766ba9f9 100644 --- a/src/connectors/meteora/amm-routes/executeSwap.ts +++ b/src/connectors/meteora/amm-routes/executeSwap.ts @@ -61,19 +61,18 @@ export async function executeSwap( }); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Re-fetch with retry; a landed-but-failed transaction throws instead of being + // misreported as confirmed or pending. + const txData = await solana.getConfirmedTransactionData(signature); const result = await solana.handleConfirmation( signature, - txData !== null, txData, quote.inputMint.toBase58(), quote.outputMint.toBase58(), walletAddress, side, + slippagePct, ); return result as ExecuteSwapResponseType; diff --git a/src/connectors/meteora/amm-routes/removeLiquidity.ts b/src/connectors/meteora/amm-routes/removeLiquidity.ts index 6a8f15839b..a2ffc3c75b 100644 --- a/src/connectors/meteora/amm-routes/removeLiquidity.ts +++ b/src/connectors/meteora/amm-routes/removeLiquidity.ts @@ -96,10 +96,9 @@ export async function removeLiquidity( }); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); if (txData) { const { balanceChanges } = await solana.extractBalanceChangesAndFee(signature, walletAddress, [ diff --git a/src/connectors/meteora/clmm-routes/addLiquidity.ts b/src/connectors/meteora/clmm-routes/addLiquidity.ts index dd8f1061ed..e8da979c9a 100644 --- a/src/connectors/meteora/clmm-routes/addLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/addLiquidity.ts @@ -123,10 +123,9 @@ export async function addLiquidity( const { signature, fee } = await solana.sendAndConfirmTransactionForWallet(addLiquidityTx, address); // Get transaction data for confirmation - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; diff --git a/src/connectors/meteora/clmm-routes/closePosition.ts b/src/connectors/meteora/clmm-routes/closePosition.ts index 8351b56844..7b4f225851 100644 --- a/src/connectors/meteora/clmm-routes/closePosition.ts +++ b/src/connectors/meteora/clmm-routes/closePosition.ts @@ -84,10 +84,9 @@ export async function closePosition( const signature = lastSignature; // Get transaction data for confirmation - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; diff --git a/src/connectors/meteora/clmm-routes/collectFees.ts b/src/connectors/meteora/clmm-routes/collectFees.ts index 9f1d347161..75f87899a0 100644 --- a/src/connectors/meteora/clmm-routes/collectFees.ts +++ b/src/connectors/meteora/clmm-routes/collectFees.ts @@ -72,10 +72,9 @@ export async function collectFees( const fee = totalFee; // Get transaction data for confirmation - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; diff --git a/src/connectors/meteora/clmm-routes/createPool.ts b/src/connectors/meteora/clmm-routes/createPool.ts index 3f99ac0269..d1007916f7 100644 --- a/src/connectors/meteora/clmm-routes/createPool.ts +++ b/src/connectors/meteora/clmm-routes/createPool.ts @@ -195,10 +195,9 @@ export async function createPool( } const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); if (txData) { return { diff --git a/src/connectors/meteora/clmm-routes/executeSwap.ts b/src/connectors/meteora/clmm-routes/executeSwap.ts index b35f741ef9..521ac013c7 100644 --- a/src/connectors/meteora/clmm-routes/executeSwap.ts +++ b/src/connectors/meteora/clmm-routes/executeSwap.ts @@ -91,11 +91,10 @@ export async function executeSwap( logger.info(`Transaction sent with signature: ${signature}`); - // Get transaction data for confirmation - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Get transaction data for confirmation. The retrying fetch throws the shared + // landed-but-failed error when the transaction landed with an error, so existence of + // txData below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; @@ -135,6 +134,7 @@ export async function executeSwap( fee: txFee, baseTokenBalanceChange, quoteTokenBalanceChange, + slippagePct, }, }; } else { diff --git a/src/connectors/meteora/clmm-routes/openPosition.ts b/src/connectors/meteora/clmm-routes/openPosition.ts index d60340c678..5c6f443b67 100644 --- a/src/connectors/meteora/clmm-routes/openPosition.ts +++ b/src/connectors/meteora/clmm-routes/openPosition.ts @@ -174,10 +174,9 @@ export async function openPosition( ]); // Get transaction data for confirmation - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; diff --git a/src/connectors/meteora/clmm-routes/removeLiquidity.ts b/src/connectors/meteora/clmm-routes/removeLiquidity.ts index a661372802..b468299de8 100644 --- a/src/connectors/meteora/clmm-routes/removeLiquidity.ts +++ b/src/connectors/meteora/clmm-routes/removeLiquidity.ts @@ -97,10 +97,9 @@ export async function removeLiquidity( const fee = totalFee; // Get transaction data for confirmation - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; diff --git a/src/connectors/okx/router-routes/executeQuote.ts b/src/connectors/okx/router-routes/executeQuote.ts index a99b018951..d66d14dfe0 100644 --- a/src/connectors/okx/router-routes/executeQuote.ts +++ b/src/connectors/okx/router-routes/executeQuote.ts @@ -41,18 +41,18 @@ export async function executeQuote( ); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Re-fetch with retry; a landed-but-failed transaction throws instead of being + // misreported as confirmed or pending. + const txData = await solana.getConfirmedTransactionData(signature); const result = await solana.handleConfirmation( signature, - txData !== null, txData, inputToken.address, outputToken.address, walletAddress, + undefined, + slippagePct, ); // Remove quote from cache only after successful execution (confirmed) diff --git a/src/connectors/orca/clmm-routes/closePosition.ts b/src/connectors/orca/clmm-routes/closePosition.ts index 17cb745e6e..c6b2584569 100644 --- a/src/connectors/orca/clmm-routes/closePosition.ts +++ b/src/connectors/orca/clmm-routes/closePosition.ts @@ -54,10 +54,9 @@ export async function closePosition( const transaction = buildOrcaTransaction(closeResult.instructions, walletAddress); const { signature, fee } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); let positionRentRefunded = 0; if (txData) { diff --git a/src/connectors/orca/clmm-routes/createPool.ts b/src/connectors/orca/clmm-routes/createPool.ts index f44fd8ba48..560bcf74b4 100644 --- a/src/connectors/orca/clmm-routes/createPool.ts +++ b/src/connectors/orca/clmm-routes/createPool.ts @@ -144,10 +144,9 @@ export async function createPool( const transaction = buildOrcaTransaction(instructions, walletAddress); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress, extraSigners); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); if (txData) { return { diff --git a/src/connectors/orca/clmm-routes/executeSwap.ts b/src/connectors/orca/clmm-routes/executeSwap.ts index 84b0318b5a..6595f14a17 100644 --- a/src/connectors/orca/clmm-routes/executeSwap.ts +++ b/src/connectors/orca/clmm-routes/executeSwap.ts @@ -118,6 +118,7 @@ export async function executeSwap( fee, baseTokenBalanceChange, quoteTokenBalanceChange, + slippagePct, }, }; } diff --git a/src/connectors/orca/clmm-routes/openPosition.ts b/src/connectors/orca/clmm-routes/openPosition.ts index b0877b9ecb..8301ab4c3f 100644 --- a/src/connectors/orca/clmm-routes/openPosition.ts +++ b/src/connectors/orca/clmm-routes/openPosition.ts @@ -219,10 +219,9 @@ export async function openPosition( const { signature, fee } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress, [ positionMintKeypair, ]); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); let positionRent = 0; let baseTokenAmountAdded = liquidityQuote ? Number(liquidityQuote.tokenEstA) / 10 ** mintA.data.decimals : 0; diff --git a/src/connectors/pancakeswap-sol/clmm-routes/addLiquidity.ts b/src/connectors/pancakeswap-sol/clmm-routes/addLiquidity.ts index e9c881a774..04b64e0911 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/addLiquidity.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/addLiquidity.ts @@ -130,6 +130,10 @@ export async function addLiquidity( }; } + // A landed-but-failed transaction is terminal: fail loudly instead of returning + // PENDING (callers would poll forever). Genuinely-not-landed keeps the pending shape. + await solana.throwIfLandedWithError(signature, txData); + return { signature, status: 0, // PENDING diff --git a/src/connectors/pancakeswap-sol/clmm-routes/closePosition.ts b/src/connectors/pancakeswap-sol/clmm-routes/closePosition.ts index a945fc6bb6..9b31efa55d 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/closePosition.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/closePosition.ts @@ -129,6 +129,10 @@ export async function closePosition( }; } + // A landed-but-failed transaction is terminal: fail loudly instead of returning + // PENDING (callers would poll forever). Genuinely-not-landed keeps the pending shape. + await solana.throwIfLandedWithError(signature, txData); + return { signature, status: 0, // PENDING diff --git a/src/connectors/pancakeswap-sol/clmm-routes/collectFees.ts b/src/connectors/pancakeswap-sol/clmm-routes/collectFees.ts index b8bd781e1c..46d900fe71 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/collectFees.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/collectFees.ts @@ -1,46 +1,107 @@ import { Static } from '@sinclair/typebox'; +import { PublicKey } from '@solana/web3.js'; +import BN from 'bn.js'; import { FastifyPluginAsync } from 'fastify'; +import { Solana } from '../../../chains/solana/solana'; import { CollectFeesResponse, CollectFeesResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; +import { PancakeswapSol } from '../pancakeswap-sol'; +import { buildRemoveLiquidityTransaction } from '../pancakeswap-sol.transactions'; import { PancakeswapSolClmmCollectFeesRequest } from '../schemas'; -import { removeLiquidity } from './removeLiquidity'; - +/** + * Collect accumulated fees from a position WITHOUT touching its liquidity. + * + * The PancakeSwap Solana CLMM program (a Raydium CLMM fork) has no owner-facing + * "collect fees" instruction — like Raydium, fees (and rewards) owed to a position are + * transferred by `decrease_liquidity_v2`. Calling it with `liquidity = 0` collects the + * owed fees while leaving the position's liquidity intact. + */ async function collectFees( network: string, walletAddress: string, positionAddress: string, ): Promise { - logger.info(`Collecting fees from position ${positionAddress} by removing 1% liquidity`); + const solana = await Solana.getInstance(network); + const pancakeswapSol = await PancakeswapSol.getInstance(network); + + const positionInfo = await pancakeswapSol.getPositionInfo(positionAddress); + if (!positionInfo) { + throw httpErrors.notFound(`Position not found: ${positionAddress}`); + } + + const baseToken = await solana.getToken(positionInfo.baseTokenAddress); + const quoteToken = await solana.getToken(positionInfo.quoteTokenAddress); + if (!baseToken || !quoteToken) { + throw httpErrors.notFound('Token information not found'); + } + + logger.info(`Collecting fees from position ${positionAddress} via zero-liquidity decrease`); + + const wallet = await solana.getWallet(walletAddress); + const walletPubkey = new PublicKey(walletAddress); + const positionNftMint = new PublicKey(positionAddress); + + // Get priority fee + const priorityFeeInLamports = await solana.estimateGasPrice(); + const priorityFeePerCU = Math.floor(priorityFeeInLamports * 1e6); + + // decrease_liquidity_v2 with liquidity = 0 transfers the owed fees (and rewards) + // without removing any liquidity. + const transaction = await buildRemoveLiquidityTransaction( + solana, + positionNftMint, + walletPubkey, + new BN(0), // liquidity: collect fees only + new BN(0), // amount0Min + new BN(0), // amount1Min + 600000, // Compute units + priorityFeePerCU, + ); + + // Sign and send + transaction.sign([wallet]); + await solana.simulateWithErrorHandling(transaction); + + const { confirmed, signature, txData } = await solana.sendAndConfirmRawTransaction(transaction); + + if (confirmed && txData) { + const totalFee = txData.meta.fee; + + // No liquidity was removed, so the position tokens received are exactly the fees. + const { balanceChanges } = await solana.extractBalanceChangesAndFee(signature, walletAddress, [ + baseToken.address, + quoteToken.address, + ]); - // Use the clever Raydium approach: remove 1% of liquidity to collect fees - // This withdraws a tiny amount of liquidity + all accumulated fees - const removeLiquidityResponse = await removeLiquidity(network, walletAddress, positionAddress, 1); + const baseFeeCollected = Math.abs(balanceChanges[0]); + const quoteFeeCollected = Math.abs(balanceChanges[1]); + + logger.info( + `Fees collected from position ${positionAddress}: ${baseFeeCollected.toFixed(6)} ${baseToken.symbol}, ` + + `${quoteFeeCollected.toFixed(6)} ${quoteToken.symbol}`, + ); - if (removeLiquidityResponse.status !== 1 || !removeLiquidityResponse.data) { return { - signature: removeLiquidityResponse.signature, - status: removeLiquidityResponse.status, + signature, + status: 1, // CONFIRMED + data: { + fee: totalFee / 1e9, + baseFeeAmountCollected: baseFeeCollected, + quoteFeeAmountCollected: quoteFeeCollected, + }, }; } - // The fees are included in the amounts removed - // Since we only removed 1%, most of the tokens received are fees - const baseFeeCollected = removeLiquidityResponse.data.baseTokenAmountRemoved; - const quoteFeeCollected = removeLiquidityResponse.data.quoteTokenAmountRemoved; - - logger.info(`Fees collected. Base: ${baseFeeCollected}, Quote: ${quoteFeeCollected}`); + // A landed-but-failed transaction is terminal: fail loudly instead of returning + // PENDING (callers would poll forever). Genuinely-not-landed keeps the pending shape. + await solana.throwIfLandedWithError(signature, txData); return { - signature: removeLiquidityResponse.signature, - status: 1, // CONFIRMED - data: { - fee: removeLiquidityResponse.data.fee, - baseFeeAmountCollected: baseFeeCollected, - quoteFeeAmountCollected: quoteFeeCollected, - }, + signature, + status: 0, // PENDING }; } @@ -54,7 +115,8 @@ export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { '/collect-fees', { schema: { - description: 'Collect accumulated fees from a PancakeSwap Solana CLMM position (removes 1% liquidity)', + description: + 'Collect accumulated fees from a PancakeSwap Solana CLMM position (zero-liquidity decrease; liquidity is not touched)', tags: ['/connector/pancakeswap-sol'], body: PancakeswapSolClmmCollectFeesRequest, response: { diff --git a/src/connectors/pancakeswap-sol/clmm-routes/createPool.ts b/src/connectors/pancakeswap-sol/clmm-routes/createPool.ts index b9740a6579..c8578909e2 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/createPool.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/createPool.ts @@ -223,6 +223,10 @@ export async function createPool( }; } + // A landed-but-failed transaction is terminal: fail loudly instead of returning + // PENDING (callers would poll forever). Genuinely-not-landed keeps the pending shape. + await solana.throwIfLandedWithError(signature, txData); + return { signature, status: 0, poolAddress, price: seedPrice }; // PENDING } diff --git a/src/connectors/pancakeswap-sol/clmm-routes/executeSwap.ts b/src/connectors/pancakeswap-sol/clmm-routes/executeSwap.ts index a291385bbe..a53d609e58 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/executeSwap.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/executeSwap.ts @@ -179,10 +179,14 @@ export async function executeSwap( fee: totalFee / 1e9, baseTokenBalanceChange: baseTokenChange, quoteTokenBalanceChange: quoteTokenChange, + slippagePct: quote.slippagePct, }, }; } else { - // Transaction pending + // A landed-but-failed transaction is terminal: fail loudly instead of returning + // PENDING (callers would poll forever). Genuinely-not-landed keeps the pending shape. + await solana.throwIfLandedWithError(signature, txData); + return { signature, status: 0, // PENDING diff --git a/src/connectors/pancakeswap-sol/clmm-routes/openPosition.ts b/src/connectors/pancakeswap-sol/clmm-routes/openPosition.ts index 699a811ced..2ee868f187 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/openPosition.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/openPosition.ts @@ -173,6 +173,10 @@ export async function openPosition( }; } + // A landed-but-failed transaction is terminal: fail loudly instead of returning + // PENDING (callers would poll forever). Genuinely-not-landed keeps the pending shape. + await solana.throwIfLandedWithError(signature, txData); + return { signature, status: 0, // PENDING diff --git a/src/connectors/pancakeswap-sol/clmm-routes/removeLiquidity.ts b/src/connectors/pancakeswap-sol/clmm-routes/removeLiquidity.ts index baaba8fa4e..8fc910ba7f 100644 --- a/src/connectors/pancakeswap-sol/clmm-routes/removeLiquidity.ts +++ b/src/connectors/pancakeswap-sol/clmm-routes/removeLiquidity.ts @@ -114,6 +114,10 @@ export async function removeLiquidity( }; } + // A landed-but-failed transaction is terminal: fail loudly instead of returning + // PENDING (callers would poll forever). Genuinely-not-landed keeps the pending shape. + await solana.throwIfLandedWithError(signature, txData); + return { signature, status: 0, // PENDING diff --git a/src/connectors/pancakeswap/amm-routes/executeSwap.ts b/src/connectors/pancakeswap/amm-routes/executeSwap.ts index a0a2584c0a..ba13de036b 100644 --- a/src/connectors/pancakeswap/amm-routes/executeSwap.ts +++ b/src/connectors/pancakeswap/amm-routes/executeSwap.ts @@ -259,6 +259,7 @@ export async function executeAmmSwap( fee: gasFee, baseTokenBalanceChange, quoteTokenBalanceChange, + slippagePct, }, }; } catch (error) { diff --git a/src/connectors/pancakeswap/clmm-routes/executeSwap.ts b/src/connectors/pancakeswap/clmm-routes/executeSwap.ts index 47fd5e91da..d14aae5085 100644 --- a/src/connectors/pancakeswap/clmm-routes/executeSwap.ts +++ b/src/connectors/pancakeswap/clmm-routes/executeSwap.ts @@ -292,6 +292,7 @@ export async function executeClmmSwap( fee: gasFee, baseTokenBalanceChange, quoteTokenBalanceChange, + slippagePct, }, }; } catch (error) { diff --git a/src/connectors/pancakeswap/router-routes/executeQuote.ts b/src/connectors/pancakeswap/router-routes/executeQuote.ts index cbb6ae95cf..fff99c8780 100644 --- a/src/connectors/pancakeswap/router-routes/executeQuote.ts +++ b/src/connectors/pancakeswap/router-routes/executeQuote.ts @@ -18,7 +18,7 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str } const { quote, request } = cached; - const { inputToken, outputToken, side, amount } = request; + const { inputToken, outputToken, side, amount, slippagePct } = request; const ethereum = await Ethereum.getInstance(network); @@ -179,6 +179,8 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str expectedAmountIn, expectedAmountOut, side, + undefined, + slippagePct, ); // Handle different transaction states diff --git a/src/connectors/raydium/amm-routes/addLiquidity.ts b/src/connectors/raydium/amm-routes/addLiquidity.ts index 33ed4a8213..05e33bf3dd 100644 --- a/src/connectors/raydium/amm-routes/addLiquidity.ts +++ b/src/connectors/raydium/amm-routes/addLiquidity.ts @@ -188,10 +188,9 @@ export async function addLiquidity( // Sign + send via the wallet-type-aware chokepoint (handles local/hardware and // simulates internally). const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; if (confirmed && txData) { diff --git a/src/connectors/raydium/amm-routes/createPool.ts b/src/connectors/raydium/amm-routes/createPool.ts index 39a9d54a4c..be3d07b5dc 100644 --- a/src/connectors/raydium/amm-routes/createPool.ts +++ b/src/connectors/raydium/amm-routes/createPool.ts @@ -186,10 +186,9 @@ export async function createPool( logger.info(`Creating Raydium CPMM pool ${poolAddress} (${baseToken}/${quoteToken})`); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); if (txData) { const { balanceChanges } = await solana.extractBalanceChangesAndFee(signature, walletAddress, [ diff --git a/src/connectors/raydium/amm-routes/executeSwap.ts b/src/connectors/raydium/amm-routes/executeSwap.ts index 273e56f1c4..dd4a75ee3b 100644 --- a/src/connectors/raydium/amm-routes/executeSwap.ts +++ b/src/connectors/raydium/amm-routes/executeSwap.ts @@ -156,20 +156,19 @@ export async function executeSwap( // Sign + send via the wallet-type-aware chokepoint (handles local/hardware and // simulates internally). const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Re-fetch with retry; a landed-but-failed transaction throws instead of being + // misreported as confirmed or pending. + const txData = await solana.getConfirmedTransactionData(signature); // Handle confirmation status const result = await solana.handleConfirmation( signature, - txData !== null, txData, inputToken.address, outputToken.address, walletAddress, side, + effectiveSlippage, ); if (result.status === 1) { diff --git a/src/connectors/raydium/amm-routes/removeLiquidity.ts b/src/connectors/raydium/amm-routes/removeLiquidity.ts index 806801bcc3..cba49ce3ca 100644 --- a/src/connectors/raydium/amm-routes/removeLiquidity.ts +++ b/src/connectors/raydium/amm-routes/removeLiquidity.ts @@ -189,10 +189,9 @@ export async function removeLiquidity( // Sign + send via the wallet-type-aware chokepoint (handles local/hardware and // simulates internally). const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; if (confirmed && txData) { diff --git a/src/connectors/raydium/clmm-routes/addLiquidity.ts b/src/connectors/raydium/clmm-routes/addLiquidity.ts index f697aaef23..6019cf658f 100644 --- a/src/connectors/raydium/clmm-routes/addLiquidity.ts +++ b/src/connectors/raydium/clmm-routes/addLiquidity.ts @@ -81,10 +81,9 @@ export async function addLiquidity( // Sign + send via the wallet-type-aware chokepoint (handles local/hardware and // simulates internally). const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; if (confirmed && txData) { diff --git a/src/connectors/raydium/clmm-routes/closePosition.ts b/src/connectors/raydium/clmm-routes/closePosition.ts index 8336fe294f..74bc1386da 100644 --- a/src/connectors/raydium/clmm-routes/closePosition.ts +++ b/src/connectors/raydium/clmm-routes/closePosition.ts @@ -102,10 +102,9 @@ export async function closePosition( // Sign + send via the wallet-type-aware chokepoint (handles local/hardware and // simulates internally). const { signature } = await solana.sendAndConfirmTransactionForWallet(result.transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; if (!confirmed || !txData) { diff --git a/src/connectors/raydium/clmm-routes/createPool.ts b/src/connectors/raydium/clmm-routes/createPool.ts index 919dd73689..72e96892f4 100644 --- a/src/connectors/raydium/clmm-routes/createPool.ts +++ b/src/connectors/raydium/clmm-routes/createPool.ts @@ -184,10 +184,9 @@ export async function createPool( walletAddress, (sdkSigners as Keypair[]) ?? [], ); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); if (txData) { return { diff --git a/src/connectors/raydium/clmm-routes/executeSwap.ts b/src/connectors/raydium/clmm-routes/executeSwap.ts index beeb39a39b..2b18b2dc7d 100644 --- a/src/connectors/raydium/clmm-routes/executeSwap.ts +++ b/src/connectors/raydium/clmm-routes/executeSwap.ts @@ -154,20 +154,19 @@ export async function executeSwap( // Sign + send via the wallet-type-aware chokepoint (handles local/hardware and // simulates internally). const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Re-fetch with retry; a landed-but-failed transaction throws instead of being + // misreported as confirmed or pending. + const txData = await solana.getConfirmedTransactionData(signature); // Handle confirmation status const result = await solana.handleConfirmation( signature, - txData !== null, txData, inputToken.address, outputToken.address, walletAddress, side, + slippagePct, ); if (result.status === 1) { diff --git a/src/connectors/raydium/clmm-routes/openPosition.ts b/src/connectors/raydium/clmm-routes/openPosition.ts index 5f046e2903..b4e4e30e00 100644 --- a/src/connectors/raydium/clmm-routes/openPosition.ts +++ b/src/connectors/raydium/clmm-routes/openPosition.ts @@ -112,10 +112,9 @@ export async function openPosition( walletAddress, (sdkSigners as Keypair[]) ?? [], ); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; // Return with status diff --git a/src/connectors/raydium/clmm-routes/removeLiquidity.ts b/src/connectors/raydium/clmm-routes/removeLiquidity.ts index 668b1c09ba..12d2eb0f22 100644 --- a/src/connectors/raydium/clmm-routes/removeLiquidity.ts +++ b/src/connectors/raydium/clmm-routes/removeLiquidity.ts @@ -76,10 +76,9 @@ export async function removeLiquidity( // Sign + send via the wallet-type-aware chokepoint (handles local/hardware and // simulates internally). const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); const confirmed = txData !== null; // Return with status diff --git a/src/connectors/titan/router-routes/executeQuote.ts b/src/connectors/titan/router-routes/executeQuote.ts index 26407e0eb2..ed65bf8fed 100644 --- a/src/connectors/titan/router-routes/executeQuote.ts +++ b/src/connectors/titan/router-routes/executeQuote.ts @@ -19,7 +19,7 @@ export async function executeQuote( throw httpErrors.badRequest('Quote not found or expired'); } - const { wallet, inputToken, outputToken, swapRoute } = cached; + const { wallet, inputToken, outputToken, swapRoute, slippagePct } = cached; // Titan instructions are built for a specific wallet; executing them from another wallet // would fail on-chain or move the wrong accounts — require a re-quote instead @@ -43,18 +43,18 @@ export async function executeQuote( ); const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); - const txData = await solana.connection.getTransaction(signature, { - commitment: 'confirmed', - maxSupportedTransactionVersion: 0, - }); + // Re-fetch with retry; a landed-but-failed transaction throws instead of being + // misreported as confirmed or pending. + const txData = await solana.getConfirmedTransactionData(signature); const result = await solana.handleConfirmation( signature, - txData !== null, txData, inputToken.address, outputToken.address, walletAddress, + undefined, + slippagePct, ); // Remove quote from cache only after successful execution (confirmed) diff --git a/src/connectors/uniswap/amm-routes/executeSwap.ts b/src/connectors/uniswap/amm-routes/executeSwap.ts index d9830dd32b..b67af4d88b 100644 --- a/src/connectors/uniswap/amm-routes/executeSwap.ts +++ b/src/connectors/uniswap/amm-routes/executeSwap.ts @@ -251,6 +251,7 @@ export async function executeAmmSwap( fee: gasFee, baseTokenBalanceChange, quoteTokenBalanceChange, + slippagePct, }, }; } catch (error) { diff --git a/src/connectors/uniswap/clmm-routes/executeSwap.ts b/src/connectors/uniswap/clmm-routes/executeSwap.ts index 152d02e0d7..5c3c13eaa4 100644 --- a/src/connectors/uniswap/clmm-routes/executeSwap.ts +++ b/src/connectors/uniswap/clmm-routes/executeSwap.ts @@ -285,6 +285,7 @@ export async function executeClmmSwap( fee: gasFee, baseTokenBalanceChange, quoteTokenBalanceChange, + slippagePct, }, }; } catch (error) { diff --git a/src/connectors/uniswap/router-routes/executeQuote.ts b/src/connectors/uniswap/router-routes/executeQuote.ts index 7f6e4f8739..ce43d08b42 100644 --- a/src/connectors/uniswap/router-routes/executeQuote.ts +++ b/src/connectors/uniswap/router-routes/executeQuote.ts @@ -22,7 +22,7 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str } const { quote, request } = cached; - const { inputToken, outputToken, side, amount } = request; + const { inputToken, outputToken, side, amount, slippagePct } = request; const ethereum = await Ethereum.getInstance(network); @@ -277,6 +277,7 @@ async function executeQuote(walletAddress: string, network: string, quoteId: str expectedAmountOut, side, txHash, + slippagePct, ); // Handle different transaction states diff --git a/src/schemas/amm-schema.ts b/src/schemas/amm-schema.ts index a9b678ff90..79a0062540 100644 --- a/src/schemas/amm-schema.ts +++ b/src/schemas/amm-schema.ts @@ -283,6 +283,9 @@ export const ExecuteSwapResponse = Type.Object( fee: Type.Number(), baseTokenBalanceChange: Type.Number(), quoteTokenBalanceChange: Type.Number(), + slippagePct: Type.Optional( + Type.Number({ description: 'Slippage tolerance percentage actually applied to the swap' }), + ), }), ), }, diff --git a/src/schemas/chain-schema.ts b/src/schemas/chain-schema.ts index b74e240bd0..400cd4e878 100644 --- a/src/schemas/chain-schema.ts +++ b/src/schemas/chain-schema.ts @@ -222,6 +222,11 @@ export const ChainExecuteSwapResponseSchema = Type.Object( quoteTokenBalanceChange: Type.Number({ description: 'Change in quote token balance (negative for decrease)', }), + slippagePct: Type.Optional( + Type.Number({ + description: 'Slippage tolerance percentage actually applied to the swap', + }), + ), }), ), }, diff --git a/src/schemas/clmm-schema.ts b/src/schemas/clmm-schema.ts index 23f10f09b0..30c8d56999 100644 --- a/src/schemas/clmm-schema.ts +++ b/src/schemas/clmm-schema.ts @@ -492,6 +492,9 @@ export const ExecuteSwapResponse = Type.Object( fee: Type.Number(), baseTokenBalanceChange: Type.Number(), quoteTokenBalanceChange: Type.Number(), + slippagePct: Type.Optional( + Type.Number({ description: 'Slippage tolerance percentage actually applied to the swap' }), + ), }), ), }, diff --git a/src/schemas/router-schema.ts b/src/schemas/router-schema.ts index 9ee6cc952f..9fb382d98e 100644 --- a/src/schemas/router-schema.ts +++ b/src/schemas/router-schema.ts @@ -183,6 +183,11 @@ export const SwapExecuteResponse = Type.Object( quoteTokenBalanceChange: Type.Number({ description: 'Change in quote token balance (negative for decrease)', }), + slippagePct: Type.Optional( + Type.Number({ + description: 'Slippage tolerance percentage actually applied to the swap', + }), + ), }), ), }, diff --git a/src/services/error-handler.ts b/src/services/error-handler.ts index dfb4911592..49d4baf5b1 100644 --- a/src/services/error-handler.ts +++ b/src/services/error-handler.ts @@ -5,6 +5,7 @@ export const ErrorCode = { TRANSACTION_TIMEOUT: 'TRANSACTION_TIMEOUT', // Retryable - tx may have succeeded SIMULATION_FAILED: 'SIMULATION_FAILED', // Non-retryable - tx would fail on-chain + TRANSACTION_FAILED: 'TRANSACTION_FAILED', // Non-retryable - tx landed on-chain but failed (fees were paid) INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE', // Non-retryable - not enough funds INVALID_PARAMS: 'INVALID_PARAMS', // Non-retryable - bad request params SLIPPAGE_EXCEEDED: 'SLIPPAGE_EXCEEDED', // Non-retryable - price moved too much @@ -88,6 +89,10 @@ export function simulationFailed(message: string): HttpError { return new HttpError(400, message, ErrorCode.SIMULATION_FAILED); } +export function transactionFailed(message: string): HttpError { + return new HttpError(400, message, ErrorCode.TRANSACTION_FAILED); +} + export function insufficientBalance(message: string): HttpError { return new HttpError(400, message, ErrorCode.INSUFFICIENT_BALANCE); } @@ -115,6 +120,7 @@ export const httpErrors = { forbidden, transactionTimeout, simulationFailed, + transactionFailed, insufficientBalance, slippageExceeded, noRouteFound, diff --git a/src/services/quote-cache.ts b/src/services/quote-cache.ts index c9ea946a11..05bf1ba8cd 100644 --- a/src/services/quote-cache.ts +++ b/src/services/quote-cache.ts @@ -41,6 +41,20 @@ class QuoteCache { return cached.quote; } + /** + * Get the original request data stored alongside a quote + * @param quoteId The unique quote identifier + * @returns The cached request data or null if not found + */ + public getRequest(quoteId: string): any | null { + const cached = this.cache.get(quoteId); + if (!cached) { + return null; + } + + return cached.request; + } + /** * Store a quote in cache * @param quoteId The unique quote identifier diff --git a/test/chains/solana/confirmation-helpers.test.ts b/test/chains/solana/confirmation-helpers.test.ts new file mode 100644 index 0000000000..a868dd67e5 --- /dev/null +++ b/test/chains/solana/confirmation-helpers.test.ts @@ -0,0 +1,134 @@ +/** + * Unit tests for the shared route-level confirmation helpers: + * Solana.getConfirmedTransactionData and Solana.handleConfirmation. + * + * Exercised via Function.prototype.call against a hand-built `this` (like the + * sendAndConfirmTransactionForWallet tests) so the branching can be asserted without + * standing up the Solana singleton / RPC. + * + * These pin the G4 contract: existence of txData is never equated with confirmation — + * a landed-but-failed transaction throws TRANSACTION_FAILED, and a just-confirmed + * transaction whose data lags RPC visibility is found via the retrying fetch instead of + * being misreported as PENDING. + */ + +import { Solana } from '../../../src/chains/solana/solana'; + +const buildLandedWithErrorException = (Solana.prototype as any).buildLandedWithErrorException; + +const getConfirmedTransactionData = (Solana.prototype as any).getConfirmedTransactionData as ( + this: unknown, + signature: string, +) => Promise; + +const handleConfirmation = (Solana.prototype as any).handleConfirmation as ( + this: unknown, + signature: string, + txData: any, + tokenIn: string, + tokenOut: string, + walletAddress: string, + side?: 'BUY' | 'SELL', + slippagePct?: number, +) => Promise<{ signature: string; status: number; data?: any }>; + +const WALLET = 'AabEVCB1sWgCPxbn6hFYM4Ukj7UubpBRbbYqRnqRXnZD'; +const TOKEN_IN = 'So11111111111111111111111111111111111111112'; +const TOKEN_OUT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'; + +const failedTxData = { + meta: { err: { InstructionError: [1, { Custom: 6001 }] }, logMessages: [] }, +}; + +describe('Solana.getConfirmedTransactionData', () => { + it('uses the retrying fetch and returns the transaction data when it succeeded', async () => { + const fetchWithRetry = jest.fn(async () => ({ meta: { err: null, fee: 5000 } })); + const fakeThis = { _fetchTransactionWithRetry: fetchWithRetry, buildLandedWithErrorException }; + + const txData = await getConfirmedTransactionData.call(fakeThis, 'sig'); + expect(txData).toEqual({ meta: { err: null, fee: 5000 } }); + expect(fetchWithRetry).toHaveBeenCalledWith('sig'); + }); + + it('throws TRANSACTION_FAILED when the transaction landed on-chain with an error', async () => { + const fakeThis = { + _fetchTransactionWithRetry: jest.fn(async () => failedTxData), + buildLandedWithErrorException, + }; + + const error = await getConfirmedTransactionData.call(fakeThis, 'landed-sig').then( + () => { + throw new Error('expected getConfirmedTransactionData to throw'); + }, + (e: any) => e, + ); + expect(error.message).toMatch(/Transaction landed-sig landed on-chain but failed/); + expect(error.code).toBe('TRANSACTION_FAILED'); + expect(error.statusCode).toBe(400); + }); + + it('returns null when the transaction is genuinely not visible yet', async () => { + const fakeThis = { _fetchTransactionWithRetry: jest.fn(async () => null), buildLandedWithErrorException }; + await expect(getConfirmedTransactionData.call(fakeThis, 'gone-sig')).resolves.toBeNull(); + }); +}); + +describe('Solana.handleConfirmation', () => { + const confirmedThis = () => ({ + buildLandedWithErrorException, + _fetchTransactionWithRetry: jest.fn(async () => null), + extractBalanceChangesAndFee: jest.fn(async () => ({ balanceChanges: [-0.1, 14.85], fee: 0.000005 })), + }); + + it('reports CONFIRMED with the applied slippagePct echoed in data', async () => { + const fakeThis = confirmedThis(); + const result = await handleConfirmation.call( + fakeThis, + 'sig', + { meta: { err: null } }, + TOKEN_IN, + TOKEN_OUT, + WALLET, + 'SELL', + 0.75, + ); + + expect(result.status).toBe(1); + expect(result.data).toMatchObject({ + tokenIn: TOKEN_IN, + tokenOut: TOKEN_OUT, + amountIn: 0.1, + amountOut: 14.85, + slippagePct: 0.75, + }); + // Data was supplied, so no re-fetch happened. + expect(fakeThis._fetchTransactionWithRetry).not.toHaveBeenCalled(); + }); + + it('throws TRANSACTION_FAILED for a landed-but-failed transaction instead of misreporting it', async () => { + const fakeThis = confirmedThis(); + const error = await handleConfirmation.call(fakeThis, 'landed-sig', failedTxData, TOKEN_IN, TOKEN_OUT, WALLET).then( + () => { + throw new Error('expected handleConfirmation to throw'); + }, + (e: any) => e, + ); + expect(error.code).toBe('TRANSACTION_FAILED'); + expect(fakeThis.extractBalanceChangesAndFee).not.toHaveBeenCalled(); + }); + + it('re-fetches with retry when the caller has no txData, so a lagging RPC does not yield a false PENDING', async () => { + const fakeThis = confirmedThis(); + fakeThis._fetchTransactionWithRetry = jest.fn(async () => ({ meta: { err: null } })); + + const result = await handleConfirmation.call(fakeThis, 'sig', null, TOKEN_IN, TOKEN_OUT, WALLET); + expect(fakeThis._fetchTransactionWithRetry).toHaveBeenCalledWith('sig'); + expect(result.status).toBe(1); + }); + + it('returns the pending shape only when the transaction is genuinely not found', async () => { + const fakeThis = confirmedThis(); + const result = await handleConfirmation.call(fakeThis, 'pending-sig', null, TOKEN_IN, TOKEN_OUT, WALLET); + expect(result).toEqual({ signature: 'pending-sig', status: 0, data: undefined }); + }); +}); diff --git a/test/chains/solana/sendAndConfirmTransactionForWallet.test.ts b/test/chains/solana/sendAndConfirmTransactionForWallet.test.ts index 593612fda9..e4dec19616 100644 --- a/test/chains/solana/sendAndConfirmTransactionForWallet.test.ts +++ b/test/chains/solana/sendAndConfirmTransactionForWallet.test.ts @@ -212,6 +212,7 @@ describe('Solana.throwIfLandedWithError / confirmationTimeoutError', () => { const throwIfLandedWithError = (Solana.prototype as any).throwIfLandedWithError as ( this: unknown, signature: string, + txData?: any, ) => Promise; const confirmationTimeoutError = (Solana.prototype as any).confirmationTimeoutError as ( this: unknown, @@ -220,6 +221,7 @@ describe('Solana.throwIfLandedWithError / confirmationTimeoutError', () => { it('surfaces the on-chain program error when a tx lands but fails (not a timeout)', async () => { const fakeThis = { + buildLandedWithErrorException: (Solana.prototype as any).buildLandedWithErrorException, connection: { getTransaction: jest.fn(async () => ({ meta: { @@ -233,7 +235,35 @@ describe('Solana.throwIfLandedWithError / confirmationTimeoutError', () => { }, }; - await expect(throwIfLandedWithError.call(fakeThis, 'landed-sig')).rejects.toThrow(/landed on-chain but failed/); + const error = await throwIfLandedWithError.call(fakeThis, 'landed-sig').then( + () => { + throw new Error('expected throwIfLandedWithError to throw'); + }, + (e: any) => e, + ); + // A landed-but-failed tx paid fees on-chain — it is TRANSACTION_FAILED (4xx, + // non-retryable), not a simulation failure, and the message keeps the signature. + expect(error.message).toMatch(/Transaction landed-sig landed on-chain but failed/); + expect(error.code).toBe('TRANSACTION_FAILED'); + expect(error.statusCode).toBe(400); + }); + + it('uses caller-provided txData without a re-fetch and throws the shared landed-but-failed error', async () => { + const getTransaction = jest.fn(); + const fakeThis = { + buildLandedWithErrorException: (Solana.prototype as any).buildLandedWithErrorException, + connection: { getTransaction }, + }; + + const failedTxData = { meta: { err: { InstructionError: [0, 'Custom'] }, logMessages: [] } }; + const error = await throwIfLandedWithError.call(fakeThis as any, 'route-sig', failedTxData).then( + () => { + throw new Error('expected throwIfLandedWithError to throw'); + }, + (e: any) => e, + ); + expect(error.code).toBe('TRANSACTION_FAILED'); + expect(getTransaction).not.toHaveBeenCalled(); }); it('returns silently when the transaction succeeded or is missing', async () => { diff --git a/test/connectors/dflow/router-routes/executeQuote.test.ts b/test/connectors/dflow/router-routes/executeQuote.test.ts index b9e41f94a1..459563810a 100644 --- a/test/connectors/dflow/router-routes/executeQuote.test.ts +++ b/test/connectors/dflow/router-routes/executeQuote.test.ts @@ -53,6 +53,7 @@ describe('POST /execute-quote (dflow)', () => { const mockSolanaInstance = { sendAndConfirmTransactionForWallet, connection: { getTransaction: jest.fn(async () => ({ meta: {} })) }, + getConfirmedTransactionData: jest.fn(async () => ({ meta: {} })), handleConfirmation: jest.fn(async () => confirmedResult), }; (Solana.getInstance as jest.Mock).mockResolvedValue(mockSolanaInstance); diff --git a/test/connectors/meteora/clmm-routes/execute-swap.test.ts b/test/connectors/meteora/clmm-routes/execute-swap.test.ts index 18d2eabf52..c19c4b5a97 100644 --- a/test/connectors/meteora/clmm-routes/execute-swap.test.ts +++ b/test/connectors/meteora/clmm-routes/execute-swap.test.ts @@ -121,6 +121,10 @@ describe('POST /execute-swap', () => { blockTime: Date.now() / 1000, }), }, + getConfirmedTransactionData: jest.fn().mockResolvedValue({ + meta: { fee: 5000 }, + blockTime: Date.now() / 1000, + }), simulateWithErrorHandling: jest.fn().mockResolvedValue(undefined), extractBalanceChangesAndFee: jest.fn().mockResolvedValue({ balanceChanges: [-0.1, 14.85], @@ -168,6 +172,58 @@ describe('POST /execute-swap', () => { expect(body.data).toHaveProperty('quoteTokenBalanceChange', 14.85); expect(body.data).toHaveProperty('tokenIn', mockSOL.address); expect(body.data).toHaveProperty('tokenOut', mockUSDC.address); + // The applied slippage is echoed on the execute response. + expect(body.data).toHaveProperty('slippagePct', 1); + }); + + it('fails loudly (400 TRANSACTION_FAILED) when the transaction landed on-chain but failed', async () => { + const { transactionFailed } = jest.requireActual('../../../../src/services/error-handler'); + const extractBalanceChangesAndFee = jest.fn(); + const mockSolanaInstance = { + getWallet: jest.fn().mockResolvedValue(mockWallet), + getToken: jest.fn((t: string) => { + if (t === 'SOL' || t === mockSOL.address) return Promise.resolve(mockSOL); + if (t === 'USDC' || t === mockUSDC.address) return Promise.resolve(mockUSDC); + return Promise.resolve(null); + }), + sendAndConfirmTransactionForWallet: jest.fn().mockResolvedValue({ + signature: mockTransaction.signature, + fee: 0.000005, + }), + // The route-level re-fetch surfaces a landed-but-failed transaction as a throw — + // it must never be reported as CONFIRMED (data exists) or PENDING. + getConfirmedTransactionData: jest + .fn() + .mockRejectedValue( + transactionFailed(`Transaction ${mockTransaction.signature} landed on-chain but failed: custom error 0x1771`), + ), + extractBalanceChangesAndFee, + }; + (Solana.getInstance as jest.Mock).mockResolvedValue(mockSolanaInstance); + + (Meteora.getInstance as jest.Mock).mockResolvedValue({ + getDlmmPool: jest.fn().mockResolvedValue(mockDlmmPool), + }); + + const response = await server.inject({ + method: 'POST', + url: '/execute-swap', + payload: { + network: 'mainnet-beta', + walletAddress: '11111111111111111111111111111111', + poolAddress: mockPoolAddress, + baseToken: 'SOL', + quoteToken: 'USDC', + amount: 0.1, + side: 'SELL', + slippagePct: 1, + }, + }); + + expect(response.statusCode).toBe(400); + expect(JSON.parse(response.body).message).toMatch(/landed on-chain but failed/); + // The route must not have tried to build a CONFIRMED response. + expect(extractBalanceChangesAndFee).not.toHaveBeenCalled(); }); it('should execute a CLMM swap for BUY side', async () => { @@ -194,6 +250,10 @@ describe('POST /execute-swap', () => { blockTime: Date.now() / 1000, }), }, + getConfirmedTransactionData: jest.fn().mockResolvedValue({ + meta: { fee: 5000 }, + blockTime: Date.now() / 1000, + }), simulateWithErrorHandling: jest.fn().mockResolvedValue(undefined), extractBalanceChangesAndFee: jest.fn().mockResolvedValue({ balanceChanges: [-15, 0.1], // For BUY: first is USDC (negative), second is SOL (positive) diff --git a/test/connectors/okx/router-routes/executeQuote.test.ts b/test/connectors/okx/router-routes/executeQuote.test.ts index 4ee20ef1e6..7ac279283d 100644 --- a/test/connectors/okx/router-routes/executeQuote.test.ts +++ b/test/connectors/okx/router-routes/executeQuote.test.ts @@ -29,6 +29,7 @@ const confirmedResult = { fee: 0.000005, baseTokenBalanceChange: -0.1, quoteTokenBalanceChange: 15, + slippagePct: 0.5, }, }; @@ -53,6 +54,7 @@ describe('POST /execute-quote (okx)', () => { const mockSolanaInstance = { sendAndConfirmTransactionForWallet, connection: { getTransaction: jest.fn(async () => ({ meta: {} })) }, + getConfirmedTransactionData: jest.fn(async () => ({ meta: {} })), handleConfirmation: jest.fn(async () => confirmedResult), }; (Solana.getInstance as jest.Mock).mockResolvedValue(mockSolanaInstance); @@ -79,7 +81,10 @@ describe('POST /execute-quote (okx)', () => { }); expect(response.statusCode).toBe(200); - expect(JSON.parse(response.body)).toMatchObject({ signature: 'okx-sig', status: 1 }); + const body = JSON.parse(response.body); + expect(body).toMatchObject({ signature: 'okx-sig', status: 1 }); + // The applied slippage survives the SwapExecuteResponse serializer. + expect(body.data.slippagePct).toBe(0.5); // The route is re-fetched with the executing wallet and the cached parameters. expect(getSwapTransaction).toHaveBeenCalledWith( WALLET, @@ -90,6 +95,18 @@ describe('POST /execute-quote (okx)', () => { 0.5, ); expect(sendAndConfirmTransactionForWallet).toHaveBeenCalledWith(unsignedTx, WALLET); + // The confirmation helper receives the retry-fetched txData and the applied slippage + // so it can decide the status (never `txData !== null`) and echo slippagePct. + expect(mockSolanaInstance.getConfirmedTransactionData).toHaveBeenCalledWith('okx-sig'); + expect(mockSolanaInstance.handleConfirmation).toHaveBeenCalledWith( + 'okx-sig', + { meta: {} }, + mockSOL.address, + mockUSDC.address, + WALLET, + undefined, + 0.5, + ); expect(quoteCache.get('okx-quote-1')).toBeNull(); }); diff --git a/test/connectors/orca/clmm-routes/closePosition.test.ts b/test/connectors/orca/clmm-routes/closePosition.test.ts index 994e334d93..b075d61a0d 100644 --- a/test/connectors/orca/clmm-routes/closePosition.test.ts +++ b/test/connectors/orca/clmm-routes/closePosition.test.ts @@ -38,6 +38,7 @@ describe('closePosition', () => { (Solana.getInstance as jest.Mock).mockResolvedValue({ sendAndConfirmTransactionForWallet: sendForWallet, connection: { getTransaction: jest.fn().mockResolvedValue(null) }, + getConfirmedTransactionData: jest.fn().mockResolvedValue(null), }); const deployment = { programId: PROGRAM, configAddress: POOL }; (Orca.getInstance as jest.Mock).mockResolvedValue({ diff --git a/test/connectors/orca/clmm-routes/create-pool.test.ts b/test/connectors/orca/clmm-routes/create-pool.test.ts index f027a4383b..fe16dddf36 100644 --- a/test/connectors/orca/clmm-routes/create-pool.test.ts +++ b/test/connectors/orca/clmm-routes/create-pool.test.ts @@ -97,6 +97,7 @@ describe('POST /create-pool (Orca CLMM)', () => { getTransaction: jest.fn().mockResolvedValue({ meta: { fee: 5000 } }), }, sendAndConfirmTransactionForWallet: sendAndConfirm, + getConfirmedTransactionData: jest.fn().mockResolvedValue({ meta: { fee: 5000 } }), }); (Orca.getInstance as jest.Mock).mockResolvedValue({ solanaKitRpc: {}, diff --git a/test/connectors/orca/clmm-routes/openPosition.test.ts b/test/connectors/orca/clmm-routes/openPosition.test.ts index 7e6a147a95..183e3f0355 100644 --- a/test/connectors/orca/clmm-routes/openPosition.test.ts +++ b/test/connectors/orca/clmm-routes/openPosition.test.ts @@ -60,6 +60,7 @@ describe('openPosition', () => { (Solana.getInstance as jest.Mock).mockResolvedValue({ sendAndConfirmTransactionForWallet: sendForWallet, connection: { getTransaction: jest.fn().mockResolvedValue(null) }, + getConfirmedTransactionData: jest.fn().mockResolvedValue(null), }); (Orca.getInstance as jest.Mock).mockResolvedValue({ solanaKitRpc: { getEpochInfo: jest.fn(() => ({ send: jest.fn().mockResolvedValue({ epoch: 1n }) })) }, diff --git a/test/connectors/pancakeswap-sol/clmm-routes/collectFees.test.ts b/test/connectors/pancakeswap-sol/clmm-routes/collectFees.test.ts new file mode 100644 index 0000000000..518e98aeb2 --- /dev/null +++ b/test/connectors/pancakeswap-sol/clmm-routes/collectFees.test.ts @@ -0,0 +1,143 @@ +import BN from 'bn.js'; + +import { Solana } from '../../../../src/chains/solana/solana'; +import { PancakeswapSol } from '../../../../src/connectors/pancakeswap-sol/pancakeswap-sol'; +import { buildRemoveLiquidityTransaction } from '../../../../src/connectors/pancakeswap-sol/pancakeswap-sol.transactions'; +import { transactionFailed } from '../../../../src/services/error-handler'; +import { fastifyWithTypeProvider } from '../../../utils/testUtils'; + +jest.mock('../../../../src/chains/solana/solana'); +jest.mock('../../../../src/connectors/pancakeswap-sol/pancakeswap-sol'); +jest.mock('../../../../src/connectors/pancakeswap-sol/pancakeswap-sol.transactions'); + +const buildApp = async () => { + const server = fastifyWithTypeProvider(); + await server.register(require('@fastify/sensible')); + const { collectFeesRoute } = await import('../../../../src/connectors/pancakeswap-sol/clmm-routes/collectFees'); + await server.register(collectFeesRoute); + return server; +}; + +const WALLET = 'AabEVCB1sWgCPxbn6hFYM4Ukj7UubpBRbbYqRnqRXnZD'; +const POSITION = 'ARwi1S4DaiTG5DX7S4M4ZsrXqpMD1MrTmbu9ue2tpmEq'; + +const mockSOL = { symbol: 'SOL', address: 'So11111111111111111111111111111111111111112', decimals: 9 }; +const mockUSDC = { symbol: 'USDC', address: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', decimals: 6 }; + +const mockPositionInfo = { + baseTokenAddress: mockSOL.address, + quoteTokenAddress: mockUSDC.address, +}; + +const mockTransaction = { sign: jest.fn() }; + +const baseSolanaMock = (overrides: Record = {}) => ({ + getToken: jest.fn((t: string) => { + if (t === mockSOL.address) return Promise.resolve(mockSOL); + if (t === mockUSDC.address) return Promise.resolve(mockUSDC); + return Promise.resolve(null); + }), + getWallet: jest.fn().mockResolvedValue({ publicKey: WALLET }), + estimateGasPrice: jest.fn().mockResolvedValue(0.001), + simulateWithErrorHandling: jest.fn().mockResolvedValue(undefined), + throwIfLandedWithError: jest.fn().mockResolvedValue(undefined), + extractBalanceChangesAndFee: jest.fn().mockResolvedValue({ balanceChanges: [0.0012, 0.34] }), + ...overrides, +}); + +describe('POST /collect-fees (pancakeswap-sol)', () => { + let server: any; + + beforeAll(async () => { + server = await buildApp(); + }); + + afterAll(async () => { + await server.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + (PancakeswapSol.getInstance as jest.Mock).mockResolvedValue({ + getPositionInfo: jest.fn().mockResolvedValue(mockPositionInfo), + }); + (buildRemoveLiquidityTransaction as jest.Mock).mockResolvedValue(mockTransaction); + }); + + it('collects fees via a zero-liquidity decrease — the position liquidity is NOT touched', async () => { + const mockSolana = baseSolanaMock({ + sendAndConfirmRawTransaction: jest + .fn() + .mockResolvedValue({ confirmed: true, signature: 'collect-sig', txData: { meta: { fee: 5000 } } }), + }); + (Solana.getInstance as jest.Mock).mockResolvedValue(mockSolana); + + const response = await server.inject({ + method: 'POST', + url: '/collect-fees', + body: { network: 'mainnet-beta', walletAddress: WALLET, positionAddress: POSITION }, + }); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + expect(body).toMatchObject({ + signature: 'collect-sig', + status: 1, + data: { + fee: 5000 / 1e9, + baseFeeAmountCollected: 0.0012, + quoteFeeAmountCollected: 0.34, + }, + }); + + // The decrease_liquidity_v2 call must carry liquidity = 0 (fees only) — the old + // implementation removed 1% of the position's liquidity and lied about the fees. + const [, , , liquidityArg, amount0Min, amount1Min] = (buildRemoveLiquidityTransaction as jest.Mock).mock.calls[0]; + expect(liquidityArg).toBeInstanceOf(BN); + expect(liquidityArg.isZero()).toBe(true); + expect(amount0Min.isZero()).toBe(true); + expect(amount1Min.isZero()).toBe(true); + }); + + it('fails loudly when the collect transaction landed on-chain but failed (no silent PENDING)', async () => { + const failedTxData = { meta: { err: { InstructionError: [0, 'Custom'] } } }; + const mockSolana = baseSolanaMock({ + sendAndConfirmRawTransaction: jest + .fn() + .mockResolvedValue({ confirmed: false, signature: 'failed-sig', txData: failedTxData }), + throwIfLandedWithError: jest + .fn() + .mockRejectedValue(transactionFailed('Transaction failed-sig landed on-chain but failed: custom error')), + }); + (Solana.getInstance as jest.Mock).mockResolvedValue(mockSolana); + + const response = await server.inject({ + method: 'POST', + url: '/collect-fees', + body: { network: 'mainnet-beta', walletAddress: WALLET, positionAddress: POSITION }, + }); + + expect(response.statusCode).toBe(400); + expect(JSON.parse(response.body).message).toMatch(/landed on-chain but failed/); + expect(mockSolana.throwIfLandedWithError).toHaveBeenCalledWith('failed-sig', failedTxData); + }); + + it('keeps the pending shape when the transaction genuinely has not landed', async () => { + const mockSolana = baseSolanaMock({ + sendAndConfirmRawTransaction: jest + .fn() + .mockResolvedValue({ confirmed: false, signature: 'pending-sig', txData: null }), + }); + (Solana.getInstance as jest.Mock).mockResolvedValue(mockSolana); + + const response = await server.inject({ + method: 'POST', + url: '/collect-fees', + body: { network: 'mainnet-beta', walletAddress: WALLET, positionAddress: POSITION }, + }); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.body)).toMatchObject({ signature: 'pending-sig', status: 0 }); + expect(mockSolana.throwIfLandedWithError).toHaveBeenCalledWith('pending-sig', null); + }); +}); diff --git a/test/connectors/raydium/amm-routes/addLiquidity.test.ts b/test/connectors/raydium/amm-routes/addLiquidity.test.ts index 3445230751..4339caf0b7 100644 --- a/test/connectors/raydium/amm-routes/addLiquidity.test.ts +++ b/test/connectors/raydium/amm-routes/addLiquidity.test.ts @@ -122,6 +122,7 @@ const buildSolanaMock = (overrides: any = {}) => ({ connection: { getTransaction: jest.fn().mockResolvedValue({ meta: { fee: 5000 } }), }, + getConfirmedTransactionData: jest.fn().mockResolvedValue({ meta: { fee: 5000 } }), extractBalanceChangesAndFee: jest.fn().mockResolvedValue({ balanceChanges: [-0.999, -149.85], }), diff --git a/test/connectors/raydium/clmm-routes/openPosition.test.ts b/test/connectors/raydium/clmm-routes/openPosition.test.ts index 55fd11f5f1..2e9d7cbef1 100644 --- a/test/connectors/raydium/clmm-routes/openPosition.test.ts +++ b/test/connectors/raydium/clmm-routes/openPosition.test.ts @@ -109,6 +109,7 @@ const buildSolanaMock = (overrides: Record = {}) => ({ connection: { getTransaction: jest.fn().mockResolvedValue(mockTxData), }, + getConfirmedTransactionData: jest.fn().mockResolvedValue(mockTxData), extractBalanceChangesAndFee: jest.fn().mockResolvedValue({ balanceChanges: [-0.002, -1, -150], }), @@ -207,10 +208,7 @@ describe('POST /open-position', () => { mockWalletAddress, mockSigners, ); - expect(mockSolanaInstance.connection.getTransaction).toHaveBeenCalledWith( - 'mock-signature', - expect.objectContaining({ commitment: 'confirmed', maxSupportedTransactionVersion: 0 }), - ); + expect(mockSolanaInstance.getConfirmedTransactionData).toHaveBeenCalledWith('mock-signature'); // Verify the response expect(body).toHaveProperty('signature', 'mock-signature'); diff --git a/test/connectors/titan/router-routes/executeQuote.test.ts b/test/connectors/titan/router-routes/executeQuote.test.ts index e66955d2eb..0ac3853f44 100644 --- a/test/connectors/titan/router-routes/executeQuote.test.ts +++ b/test/connectors/titan/router-routes/executeQuote.test.ts @@ -69,6 +69,7 @@ describe('POST /execute-quote (titan)', () => { const mockSolanaInstance = { sendAndConfirmTransactionForWallet, connection: mockConnection, + getConfirmedTransactionData: jest.fn(async () => ({ meta: {} })), handleConfirmation: jest.fn(async () => confirmedResult), }; (Solana.getInstance as jest.Mock).mockResolvedValue(mockSolanaInstance); From a6d82bb3f795998f61de3834f055fa11075e0d58 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 23:00:37 -0700 Subject: [PATCH 26/31] feat(trading): let unified swaps pin a pool by address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/trading/swap/execute.ts | 94 +++++++++---- src/trading/swap/quote.ts | 105 +++++++++++---- test/trading/swap/pool-address-pin.test.ts | 148 +++++++++++++++++++++ 3 files changed, 299 insertions(+), 48 deletions(-) create mode 100644 test/trading/swap/pool-address-pin.test.ts diff --git a/src/trading/swap/execute.ts b/src/trading/swap/execute.ts index 855f466887..d38490f5e7 100644 --- a/src/trading/swap/execute.ts +++ b/src/trading/swap/execute.ts @@ -65,6 +65,15 @@ const UnifiedExecuteSwapRequestSchema = Type.Object({ enum: ['BUY', 'SELL'], default: 'SELL', }), + poolAddress: Type.Optional( + Type.String({ + description: + 'Pin the swap to a specific pool. Only meaningful for amm/clmm providers, which trade against one ' + + 'pool; router providers choose their own route and reject it. Omit to resolve the pool from ' + + "Gateway's configured pool list by token pair — which a pool that is not in that list (a freshly " + + 'created one, an unlisted token) cannot be, so pass its address here.', + }), + ), slippagePct: slippagePctField(), approximateIfNoExactOut: Type.Optional( Type.Boolean({ @@ -90,6 +99,7 @@ async function executeSolanaSwap( slippagePct?: number, connector?: string, approximateIfNoExactOut?: boolean, + requestedPoolAddress?: string, ): Promise { try { const networkConfig = getSolanaNetworkConfig(network); @@ -102,20 +112,28 @@ async function executeSolanaSwap( `Using swap provider: ${swapProvider} for network: ${network}${connector ? ' (explicit)' : ' (from config)'}`, ); - // For AMM and CLMM, look up the pool address using PoolService - let poolAddress: string | undefined; + // An explicit pin wins; otherwise resolve the pool from the configured list. + let poolAddress: string | undefined = requestedPoolAddress; if (connectorType === 'amm' || connectorType === 'clmm') { - const poolService = PoolService.getInstance(); - const pool = await poolService.getPool('solana', network, connectorType, baseToken, quoteToken, connectorName); + if (!poolAddress) { + const poolService = PoolService.getInstance(); + const pool = await poolService.getPool('solana', network, connectorType, baseToken, quoteToken, connectorName); - if (!pool) { - throw httpErrors.notFound( - `No ${connectorType.toUpperCase()} pool found for ${baseToken}-${quoteToken} on ${connectorName}/${network}`, - ); - } + if (!pool) { + throw httpErrors.notFound( + `No ${connectorType.toUpperCase()} pool found for ${baseToken}-${quoteToken} on ${connectorName}/${network}. ` + + 'Pass poolAddress to trade against a specific pool.', + ); + } - poolAddress = pool.address; - logger.info(`Found pool: ${poolAddress} for ${baseToken}-${quoteToken}`); + poolAddress = pool.address; + logger.info(`Found pool: ${poolAddress} for ${baseToken}-${quoteToken}`); + } + } else if (requestedPoolAddress) { + throw httpErrors.badRequest( + `poolAddress is not supported for router provider ${swapProvider}: a router chooses its own route ` + + 'across pools. Use an amm or clmm provider to pin a pool.', + ); } // Route to the appropriate connector based on swapProvider @@ -209,6 +227,7 @@ async function executeEthereumSwap( side: 'BUY' | 'SELL', slippagePct?: number, connector?: string, + requestedPoolAddress?: string, ): Promise { try { const networkConfig = getEthereumNetworkConfig(network); @@ -221,20 +240,35 @@ async function executeEthereumSwap( `Using swap provider: ${swapProvider} for network: ${network}${connector ? ' (explicit)' : ' (from config)'}`, ); - // For AMM and CLMM, look up the pool address using PoolService - let poolAddress: string | undefined; + // An explicit pin wins; otherwise resolve the pool from the configured list. + let poolAddress: string | undefined = requestedPoolAddress; if (connectorType === 'amm' || connectorType === 'clmm') { - const poolService = PoolService.getInstance(); - const pool = await poolService.getPool('ethereum', network, connectorType, baseToken, quoteToken, connectorName); - - if (!pool) { - throw httpErrors.notFound( - `No ${connectorType.toUpperCase()} pool found for ${baseToken}-${quoteToken} on ${connectorName}/${network}`, + if (!poolAddress) { + const poolService = PoolService.getInstance(); + const pool = await poolService.getPool( + 'ethereum', + network, + connectorType, + baseToken, + quoteToken, + connectorName, ); - } - poolAddress = pool.address; - logger.info(`Found pool: ${poolAddress} for ${baseToken}-${quoteToken}`); + if (!pool) { + throw httpErrors.notFound( + `No ${connectorType.toUpperCase()} pool found for ${baseToken}-${quoteToken} on ${connectorName}/${network}. ` + + 'Pass poolAddress to trade against a specific pool.', + ); + } + + poolAddress = pool.address; + logger.info(`Found pool: ${poolAddress} for ${baseToken}-${quoteToken}`); + } + } else if (requestedPoolAddress) { + throw httpErrors.badRequest( + `poolAddress is not supported for router provider ${swapProvider}: a router chooses its own route ` + + 'across pools. Use an amm or clmm provider to pin a pool.', + ); } // Route to the appropriate connector based on swapProvider @@ -303,6 +337,7 @@ export async function executeUnifiedSwap( slippagePct?: number, connector?: string, approximateIfNoExactOut?: boolean, + requestedPoolAddress?: string, ): Promise { const { chain, network } = parseChainNetwork(chainNetwork); @@ -312,7 +347,17 @@ export async function executeUnifiedSwap( switch (chain.toLowerCase()) { case 'ethereum': - return executeEthereumSwap(network, walletAddress, baseToken, quoteToken, amount, side, slippagePct, connector); + return executeEthereumSwap( + network, + walletAddress, + baseToken, + quoteToken, + amount, + side, + slippagePct, + connector, + requestedPoolAddress, + ); case 'solana': return executeSolanaSwap( @@ -325,6 +370,7 @@ export async function executeUnifiedSwap( slippagePct, connector, approximateIfNoExactOut, + requestedPoolAddress, ); default: @@ -360,6 +406,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { slippagePct, approximateIfNoExactOut, connector, + poolAddress, } = request.body as UnifiedExecuteSwapRequest; try { @@ -373,6 +420,7 @@ export const executeSwapRoute: FastifyPluginAsync = async (fastify) => { slippagePct, connector, approximateIfNoExactOut, + poolAddress, ); return reply.code(200).send(result); } catch (error: any) { diff --git a/src/trading/swap/quote.ts b/src/trading/swap/quote.ts index f284657ce0..092896d3cf 100644 --- a/src/trading/swap/quote.ts +++ b/src/trading/swap/quote.ts @@ -61,6 +61,15 @@ const UnifiedQuoteSwapRequestSchema = Type.Object({ enum: ['BUY', 'SELL'], default: 'SELL', }), + poolAddress: Type.Optional( + Type.String({ + description: + 'Pin the swap to a specific pool. Only meaningful for amm/clmm providers, which trade against one ' + + 'pool; router providers choose their own route and reject it. Omit to resolve the pool from ' + + "Gateway's configured pool list by token pair — which a pool that is not in that list (a freshly " + + 'created one, an unlisted token) cannot be, so pass its address here.', + }), + ), slippagePct: slippagePctField(), approximateIfNoExactOut: Type.Optional( Type.Boolean({ @@ -85,6 +94,7 @@ async function getSolanaQuoteSwap( slippagePct?: number, connector?: string, approximateIfNoExactOut?: boolean, + requestedPoolAddress?: string, ): Promise { try { const networkConfig = getSolanaNetworkConfig(network); @@ -97,20 +107,28 @@ async function getSolanaQuoteSwap( `Using swap provider: ${swapProvider} for network: ${network}${connector ? ' (explicit)' : ' (from config)'}`, ); - // For AMM and CLMM, look up the pool address using PoolService - let poolAddress: string | undefined; + // An explicit pin wins; otherwise resolve the pool from the configured list. + let poolAddress: string | undefined = requestedPoolAddress; if (connectorType === 'amm' || connectorType === 'clmm') { - const poolService = PoolService.getInstance(); - const pool = await poolService.getPool('solana', network, connectorType, baseToken, quoteToken, connectorName); + if (!poolAddress) { + const poolService = PoolService.getInstance(); + const pool = await poolService.getPool('solana', network, connectorType, baseToken, quoteToken, connectorName); - if (!pool) { - throw httpErrors.notFound( - `No ${connectorType.toUpperCase()} pool found for ${baseToken}-${quoteToken} on ${connectorName}/${network}`, - ); - } + if (!pool) { + throw httpErrors.notFound( + `No ${connectorType.toUpperCase()} pool found for ${baseToken}-${quoteToken} on ${connectorName}/${network}. ` + + 'Pass poolAddress to trade against a specific pool.', + ); + } - poolAddress = pool.address; - logger.info(`Found pool: ${poolAddress} for ${baseToken}-${quoteToken}`); + poolAddress = pool.address; + logger.info(`Found pool: ${poolAddress} for ${baseToken}-${quoteToken}`); + } + } else if (requestedPoolAddress) { + throw httpErrors.badRequest( + `poolAddress is not supported for router provider ${swapProvider}: a router chooses its own route ` + + 'across pools. Use an amm or clmm provider to pin a pool.', + ); } // Route to the appropriate connector based on swapProvider @@ -191,6 +209,7 @@ async function getEthereumQuoteSwap( side: 'BUY' | 'SELL', slippagePct?: number, connector?: string, + requestedPoolAddress?: string, ): Promise { try { const networkConfig = getEthereumNetworkConfig(network); @@ -203,20 +222,35 @@ async function getEthereumQuoteSwap( `Using swap provider: ${swapProvider} for network: ${network}${connector ? ' (explicit)' : ' (from config)'}`, ); - // For AMM and CLMM, look up the pool address using PoolService - let poolAddress: string | undefined; + // An explicit pin wins; otherwise resolve the pool from the configured list. + let poolAddress: string | undefined = requestedPoolAddress; if (connectorType === 'amm' || connectorType === 'clmm') { - const poolService = PoolService.getInstance(); - const pool = await poolService.getPool('ethereum', network, connectorType, baseToken, quoteToken, connectorName); - - if (!pool) { - throw httpErrors.notFound( - `No ${connectorType.toUpperCase()} pool found for ${baseToken}-${quoteToken} on ${connectorName}/${network}`, + if (!poolAddress) { + const poolService = PoolService.getInstance(); + const pool = await poolService.getPool( + 'ethereum', + network, + connectorType, + baseToken, + quoteToken, + connectorName, ); - } - poolAddress = pool.address; - logger.info(`Found pool: ${poolAddress} for ${baseToken}-${quoteToken}`); + if (!pool) { + throw httpErrors.notFound( + `No ${connectorType.toUpperCase()} pool found for ${baseToken}-${quoteToken} on ${connectorName}/${network}. ` + + 'Pass poolAddress to trade against a specific pool.', + ); + } + + poolAddress = pool.address; + logger.info(`Found pool: ${poolAddress} for ${baseToken}-${quoteToken}`); + } + } else if (requestedPoolAddress) { + throw httpErrors.badRequest( + `poolAddress is not supported for router provider ${swapProvider}: a router chooses its own route ` + + 'across pools. Use an amm or clmm provider to pin a pool.', + ); } // Route to the appropriate connector based on swapProvider @@ -260,6 +294,7 @@ export async function getUnifiedQuoteSwap( slippagePct?: number, connector?: string, approximateIfNoExactOut?: boolean, + requestedPoolAddress?: string, ): Promise { const { chain, network } = parseChainNetwork(chainNetwork); @@ -269,7 +304,16 @@ export async function getUnifiedQuoteSwap( switch (chain.toLowerCase()) { case 'ethereum': - return getEthereumQuoteSwap(network, baseToken, quoteToken, amount, side, slippagePct, connector); + return getEthereumQuoteSwap( + network, + baseToken, + quoteToken, + amount, + side, + slippagePct, + connector, + requestedPoolAddress, + ); case 'solana': return getSolanaQuoteSwap( @@ -281,6 +325,7 @@ export async function getUnifiedQuoteSwap( slippagePct, connector, approximateIfNoExactOut, + requestedPoolAddress, ); default: @@ -306,8 +351,17 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { }, }, async (request, reply) => { - const { chainNetwork, baseToken, quoteToken, amount, side, slippagePct, approximateIfNoExactOut, connector } = - request.query as UnifiedQuoteSwapRequest; + const { + chainNetwork, + baseToken, + quoteToken, + amount, + side, + slippagePct, + approximateIfNoExactOut, + connector, + poolAddress, + } = request.query as UnifiedQuoteSwapRequest; try { const result = await getUnifiedQuoteSwap( @@ -319,6 +373,7 @@ export const quoteSwapRoute: FastifyPluginAsync = async (fastify) => { slippagePct, connector, approximateIfNoExactOut, + poolAddress, ); return reply.code(200).send(result); } catch (error: any) { diff --git a/test/trading/swap/pool-address-pin.test.ts b/test/trading/swap/pool-address-pin.test.ts new file mode 100644 index 0000000000..ba960d334e --- /dev/null +++ b/test/trading/swap/pool-address-pin.test.ts @@ -0,0 +1,148 @@ +import { tradingSwapRoutes } from '../../../src/trading/trading.routes'; +import { fastifyWithTypeProvider } from '../../utils/testUtils'; + +// The unified swap route resolves a pool from Gateway's configured pool list by +// token pair. A pool that is not in that list — a freshly created one, or one on +// an unlisted token — is unreachable that way, so callers can pin it by address. +// Routers choose their own route across pools and must reject the pin outright. + +const mockGetPool = jest.fn(); + +jest.mock('../../../src/services/pool-service', () => ({ + PoolService: { + getInstance: () => ({ + getPool: (...args: any[]) => mockGetPool(...args), + }), + }, +})); + +const mockMeteoraClmmQuoteSwap = jest.fn(); +jest.mock('../../../src/connectors/meteora/clmm-routes/quoteSwap', () => ({ + quoteSwap: (...args: any[]) => mockMeteoraClmmQuoteSwap(...args), +})); + +const mockJupiterRouterQuoteSwap = jest.fn(); +jest.mock('../../../src/connectors/jupiter/router-routes/quoteSwap', () => ({ + quoteSwap: (...args: any[]) => mockJupiterRouterQuoteSwap(...args), +})); + +jest.mock('../../../src/chains/solana/solana.config', () => ({ + ...jest.requireActual('../../../src/chains/solana/solana.config'), + getSolanaNetworkConfig: () => ({ swapProvider: 'jupiter/router' }), +})); + +const PINNED_POOL = '2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3'; + +const QUOTE = { + tokenIn: 'So11111111111111111111111111111111111111112', + tokenOut: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + amountIn: 1, + amountOut: 100, + price: 100, + minAmountOut: 99, + maxAmountIn: 1, + priceImpactPct: 0.1, +}; + +const buildApp = async () => { + const server = fastifyWithTypeProvider(); + await server.register(require('@fastify/sensible')); + await server.register(tradingSwapRoutes, { prefix: '/trading/swap' }); + return server; +}; + +const quoteUrl = (params: Record) => `/trading/swap/quote?${new URLSearchParams(params).toString()}`; + +describe('Unified swap quote — poolAddress pin', () => { + let app: any; + + beforeAll(async () => { + app = await buildApp(); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockMeteoraClmmQuoteSwap.mockResolvedValue(QUOTE); + mockJupiterRouterQuoteSwap.mockResolvedValue(QUOTE); + }); + + it('uses the pinned pool without consulting the configured pool list', async () => { + const response = await app.inject({ + method: 'GET', + url: quoteUrl({ + chainNetwork: 'solana-mainnet-beta', + connector: 'meteora/clmm', + baseToken: 'SOL', + quoteToken: 'USDC', + amount: '1', + side: 'SELL', + poolAddress: PINNED_POOL, + }), + }); + + expect(response.statusCode).toBe(200); + expect(mockGetPool).not.toHaveBeenCalled(); + expect(mockMeteoraClmmQuoteSwap).toHaveBeenCalledWith('mainnet-beta', PINNED_POOL, 'SOL', 'SELL', 1, undefined); + }); + + it('falls back to the configured pool list when no pin is given', async () => { + mockGetPool.mockResolvedValue({ address: PINNED_POOL }); + + const response = await app.inject({ + method: 'GET', + url: quoteUrl({ + chainNetwork: 'solana-mainnet-beta', + connector: 'meteora/clmm', + baseToken: 'SOL', + quoteToken: 'USDC', + amount: '1', + side: 'SELL', + }), + }); + + expect(response.statusCode).toBe(200); + expect(mockGetPool).toHaveBeenCalledWith('solana', 'mainnet-beta', 'clmm', 'SOL', 'USDC', 'meteora'); + }); + + it('tells an unresolvable pair that a pin is available', async () => { + mockGetPool.mockResolvedValue(null); + + const response = await app.inject({ + method: 'GET', + url: quoteUrl({ + chainNetwork: 'solana-mainnet-beta', + connector: 'meteora/clmm', + baseToken: 'NEWMINT', + quoteToken: 'SOL', + amount: '1', + side: 'SELL', + }), + }); + + expect(response.statusCode).toBe(404); + expect(response.json().message).toContain('poolAddress'); + }); + + it('rejects a pin on a router provider, which picks its own route', async () => { + const response = await app.inject({ + method: 'GET', + url: quoteUrl({ + chainNetwork: 'solana-mainnet-beta', + connector: 'jupiter/router', + baseToken: 'SOL', + quoteToken: 'USDC', + amount: '1', + side: 'SELL', + poolAddress: PINNED_POOL, + }), + }); + + expect(response.statusCode).toBe(400); + expect(response.json().message).toContain('poolAddress is not supported'); + expect(mockJupiterRouterQuoteSwap).not.toHaveBeenCalled(); + }); +}); From 1884ed7a64d2aee16a8d9556c8890d6c9bd39495 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 23:04:50 -0700 Subject: [PATCH 27/31] test(trading): assert the live field names on unified CLMM and orca removes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- .../orca/clmm-routes/removeLiquidity.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/connectors/orca/clmm-routes/removeLiquidity.test.ts b/test/connectors/orca/clmm-routes/removeLiquidity.test.ts index ffe976fdb2..b470d85535 100644 --- a/test/connectors/orca/clmm-routes/removeLiquidity.test.ts +++ b/test/connectors/orca/clmm-routes/removeLiquidity.test.ts @@ -53,7 +53,7 @@ describe('POST /remove-liquidity', () => { network: 'mainnet-beta', walletAddress: mockWalletAddress, positionAddress: mockPositionAddress, - percentage: 50, + percentageToRemove: 50, }, }); @@ -79,7 +79,7 @@ describe('POST /remove-liquidity', () => { network: 'mainnet-beta', walletAddress: mockWalletAddress, positionAddress: mockPositionAddress, - percentage: 100, + percentageToRemove: 100, }, }); @@ -92,7 +92,7 @@ describe('POST /remove-liquidity', () => { url: '/remove-liquidity', payload: { positionAddress: mockPositionAddress, - percentage: 25, + percentageToRemove: 25, }, }); @@ -108,14 +108,14 @@ describe('POST /remove-liquidity', () => { payload: { network: 'mainnet-beta', walletAddress: mockWalletAddress, - percentage: 50, + percentageToRemove: 50, }, }); expect(response.statusCode).toBe(400); }); - it('should return error when percentage is missing', async () => { + it('should return error when percentageToRemove is missing', async () => { const response = await app.inject({ method: 'POST', url: '/remove-liquidity', @@ -129,7 +129,7 @@ describe('POST /remove-liquidity', () => { expect(response.statusCode).toBeGreaterThanOrEqual(400); }); - it('should handle invalid percentage values', async () => { + it('should handle invalid percentageToRemove values', async () => { const response = await app.inject({ method: 'POST', url: '/remove-liquidity', @@ -137,7 +137,7 @@ describe('POST /remove-liquidity', () => { network: 'mainnet-beta', walletAddress: mockWalletAddress, positionAddress: mockPositionAddress, - percentage: 150, + percentageToRemove: 150, }, }); @@ -159,7 +159,7 @@ describe('POST /remove-liquidity', () => { network: 'mainnet-beta', walletAddress: mockWalletAddress, positionAddress: mockPositionAddress, - percentage: 50, + percentageToRemove: 50, }, }); @@ -176,7 +176,7 @@ describe('POST /remove-liquidity', () => { network: 'mainnet-beta', walletAddress: mockWalletAddress, positionAddress: mockPositionAddress, - percentage: 50, + percentageToRemove: 50, }, }); From 40bb747b3b5e5af316c164281f2ee44e4066dddb Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 23:05:55 -0700 Subject: [PATCH 28/31] test(trading): send chainNetwork, not the pre-fold network field Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- test/trading/clmm/routes.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/trading/clmm/routes.test.ts b/test/trading/clmm/routes.test.ts index 382cf7c543..23618b6e00 100644 --- a/test/trading/clmm/routes.test.ts +++ b/test/trading/clmm/routes.test.ts @@ -113,7 +113,7 @@ describe('Unified Trading CLMM Routes', () => { method: 'POST', url: '/trading/clmm/open', payload: { - network: 'mainnet', + chainNetwork: 'ethereum-mainnet', walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', lowerPrice: 1800, upperPrice: 2200, @@ -146,7 +146,7 @@ describe('Unified Trading CLMM Routes', () => { url: '/trading/clmm/open', payload: { connector: 'uniswap', - network: 'mainnet', + chainNetwork: 'ethereum-mainnet', lowerPrice: 1800, upperPrice: 2200, poolAddress: '0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8', @@ -178,7 +178,7 @@ describe('Unified Trading CLMM Routes', () => { url: '/trading/clmm/remove', payload: { connector: 'uniswap', - network: 'mainnet', + chainNetwork: 'ethereum-mainnet', walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', percentageToRemove: 50, }, @@ -265,7 +265,7 @@ describe('Unified Trading CLMM Routes', () => { url: '/trading/clmm/open', payload: { connector: 'uniswap', - network: 'mainnet', + chainNetwork: 'ethereum-mainnet', walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', lowerPrice: 1800, upperPrice: 2200, @@ -301,7 +301,7 @@ describe('Unified Trading CLMM Routes', () => { url: '/trading/clmm/remove', payload: { connector: 'raydium', - network: 'mainnet-beta', + chainNetwork: 'solana-mainnet-beta', walletAddress: 'BPgNwGDBiRuaAKuRQLpXC9rCiw5FfJDDdTunDEmtN6VF', positionAddress: 'position123', percentageToRemove: 50, @@ -317,7 +317,7 @@ describe('Unified Trading CLMM Routes', () => { url: '/trading/clmm/collect-fees', payload: { connector: 'meteora', - network: 'mainnet-beta', + chainNetwork: 'solana-mainnet-beta', walletAddress: 'BPgNwGDBiRuaAKuRQLpXC9rCiw5FfJDDdTunDEmtN6VF', positionAddress: 'position456', }, @@ -337,7 +337,7 @@ describe('Unified Trading CLMM Routes', () => { url: '/trading/clmm/close', payload: { connector: 'pancakeswap-sol', - network: 'mainnet-beta', + chainNetwork: 'solana-mainnet-beta', walletAddress: 'BPgNwGDBiRuaAKuRQLpXC9rCiw5FfJDDdTunDEmtN6VF', positionAddress: 'position789', }, @@ -352,7 +352,7 @@ describe('Unified Trading CLMM Routes', () => { url: '/trading/clmm/open', payload: { connector: 'invalid-connector', - network: 'mainnet', + chainNetwork: 'ethereum-mainnet', walletAddress: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb', lowerPrice: 1800, upperPrice: 2200, From 198e0d804e6a940820f02a82b0c7d47f03d9efe5 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 23:11:11 -0700 Subject: [PATCH 29/31] docs: regenerate openapi.json from the current route table 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- openapi.json | 9422 ++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 7233 insertions(+), 2189 deletions(-) diff --git a/openapi.json b/openapi.json index 6608f3c96d..8d8430f965 100644 --- a/openapi.json +++ b/openapi.json @@ -6,7 +6,15 @@ "version": "dev-2.17.0" }, "components": { - "parameters": { "queryExample": { "in": "query", "name": "example", "schema": { "type": "object" } } }, + "parameters": { + "queryExample": { + "in": "query", + "name": "example", + "schema": { + "type": "object" + } + } + }, "schemas": {} }, "paths": { @@ -16,12 +24,22 @@ "description": "Get configuration settings. Returns all configurations if no parameters are specified. Use namespace to get a specific config (e.g., server, ethereum-mainnet, solana-mainnet-beta, uniswap).", "parameters": [ { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "server": { "value": "server" }, - "ethereum-mainnet": { "value": "ethereum-mainnet" }, - "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, - "uniswap": { "value": "uniswap" } + "server": { + "value": "server" + }, + "ethereum-mainnet": { + "value": "ethereum-mainnet" + }, + "solana-mainnet-beta": { + "value": "solana-mainnet-beta" + }, + "uniswap": { + "value": "uniswap" + } }, "in": "query", "name": "namespace", @@ -32,7 +50,14 @@ "responses": { "200": { "description": "Default Response", - "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } } + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": true + } + } + } } } } @@ -60,18 +85,36 @@ "value": { "description": "Configuration value", "anyOf": [ - { "type": "string" }, - { "type": "number" }, - { "type": "boolean" }, - { "type": "object", "properties": {} }, - { "type": "array", "items": {} } + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object", + "properties": {} + }, + { + "type": "array", + "items": {} + } ] } }, "required": ["namespace", "path", "value"] }, "examples": { - "example1": { "value": { "namespace": "solana-mainnet-beta", "path": "maxFee", "value": 0.01 } }, + "example1": { + "value": { + "namespace": "solana-mainnet-beta", + "path": "maxFee", + "value": 0.01 + } + }, "example2": { "value": { "namespace": "ethereum-mainnet", @@ -80,10 +123,26 @@ } }, "example3": { - "value": { "namespace": "ethereum-mainnet", "path": "gasLimitTransaction", "value": 3000000 } + "value": { + "namespace": "ethereum-mainnet", + "path": "gasLimitTransaction", + "value": 3000000 + } + }, + "example4": { + "value": { + "namespace": "solana-devnet", + "path": "retryCount", + "value": 5 + } }, - "example4": { "value": { "namespace": "solana-devnet", "path": "retryCount", "value": 5 } }, - "example5": { "value": { "namespace": "server", "path": "port", "value": 15888 } } + "example5": { + "value": { + "namespace": "server", + "path": "port", + "value": 15888 + } + } } } }, @@ -96,7 +155,12 @@ "application/json": { "schema": { "type": "object", - "properties": { "message": { "description": "Status message", "type": "string" } }, + "properties": { + "message": { + "description": "Status message", + "type": "string" + } + }, "required": ["message"] } } @@ -122,8 +186,15 @@ "items": { "type": "object", "properties": { - "chain": { "type": "string" }, - "networks": { "type": "array", "items": { "type": "string" } } + "chain": { + "type": "string" + }, + "networks": { + "type": "array", + "items": { + "type": "string" + } + } }, "required": ["chain", "networks"] } @@ -154,10 +225,24 @@ "items": { "type": "object", "properties": { - "name": { "type": "string" }, - "trading_types": { "type": "array", "items": { "type": "string" } }, - "chain": { "type": "string" }, - "networks": { "type": "array", "items": { "type": "string" } } + "name": { + "type": "string" + }, + "trading_types": { + "type": "array", + "items": { + "type": "string" + } + }, + "chain": { + "type": "string" + }, + "networks": { + "type": "array", + "items": { + "type": "string" + } + } }, "required": ["name", "trading_types", "chain", "networks"] } @@ -182,7 +267,14 @@ "application/json": { "schema": { "type": "object", - "properties": { "namespaces": { "type": "array", "items": { "type": "string" } } }, + "properties": { + "namespaces": { + "type": "array", + "items": { + "type": "string" + } + } + }, "required": ["namespaces"] } } @@ -196,7 +288,15 @@ "tags": ["/wallet"], "description": "Get all wallets across different chains", "parameters": [ - { "schema": { "default": true, "type": "boolean" }, "in": "query", "name": "showHardware", "required": false } + { + "schema": { + "default": true, + "type": "boolean" + }, + "in": "query", + "name": "showHardware", + "required": false + } ], "responses": { "200": { @@ -208,7 +308,11 @@ "items": { "type": "object", "properties": { - "chain": { "description": "Blockchain name", "type": "string", "example": "solana" }, + "chain": { + "description": "Blockchain name", + "type": "string", + "example": "solana" + }, "walletAddresses": { "description": "List of regular wallet addresses with private keys", "type": "array", @@ -264,7 +368,11 @@ }, "required": ["chain", "privateKey"] }, - "example": { "chain": "solana", "privateKey": "", "setDefault": true } + "example": { + "chain": "solana", + "privateKey": "", + "setDefault": true + } } }, "required": true @@ -276,7 +384,12 @@ "application/json": { "schema": { "type": "object", - "properties": { "address": { "description": "The wallet address that was added", "type": "string" } }, + "properties": { + "address": { + "description": "The wallet address that was added", + "type": "string" + } + }, "required": ["address"] } } @@ -326,10 +439,22 @@ "schema": { "type": "object", "properties": { - "address": { "description": "The hardware wallet address that was added", "type": "string" }, - "publicKey": { "description": "Public key of the hardware wallet", "type": "string" }, - "derivationPath": { "description": "BIP32/BIP44 derivation path used", "type": "string" }, - "message": { "description": "Success message", "type": "string" } + "address": { + "description": "The hardware wallet address that was added", + "type": "string" + }, + "publicKey": { + "description": "Public key of the hardware wallet", + "type": "string" + }, + "derivationPath": { + "description": "BIP32/BIP44 derivation path used", + "type": "string" + }, + "message": { + "description": "Success message", + "type": "string" + } }, "required": ["address", "publicKey", "derivationPath", "message"] } @@ -355,7 +480,10 @@ "type": "string", "example": "solana" }, - "address": { "description": "Wallet address to remove", "type": "string" } + "address": { + "description": "Wallet address to remove", + "type": "string" + } }, "required": ["chain", "address"] } @@ -371,7 +499,10 @@ "schema": { "type": "object", "properties": { - "message": { "description": "Success message indicating wallet type removed", "type": "string" } + "message": { + "description": "Success message indicating wallet type removed", + "type": "string" + } }, "required": ["message"] } @@ -397,16 +528,25 @@ "type": "string", "example": "solana" }, - "address": { "description": "Wallet address to set as default", "type": "string" } + "address": { + "description": "Wallet address to set as default", + "type": "string" + } }, "required": ["chain", "address"] }, "examples": { "example1": { - "value": { "chain": "ethereum", "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2BDf8" } + "value": { + "chain": "ethereum", + "address": "0x742d35Cc6634C0532925a3b844Bc9e7595f2BDf8" + } }, "example2": { - "value": { "chain": "solana", "address": "7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi" } + "value": { + "chain": "solana", + "address": "7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi" + } } } } @@ -421,9 +561,18 @@ "schema": { "type": "object", "properties": { - "message": { "description": "Success message", "type": "string" }, - "chain": { "description": "Chain name", "type": "string" }, - "address": { "description": "Default wallet address", "type": "string" } + "message": { + "description": "Success message", + "type": "string" + }, + "chain": { + "description": "Chain name", + "type": "string" + }, + "address": { + "description": "Default wallet address", + "type": "string" + } }, "required": ["message", "chain", "address"] } @@ -439,19 +588,36 @@ "description": "Get a specific token by symbol or address", "parameters": [ { - "schema": { "type": "string" }, - "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, + "schema": { + "type": "string" + }, + "examples": { + "ethereum": { + "value": "ethereum" + }, + "solana": { + "value": "solana" + } + }, "in": "query", "name": "chain", "required": true, "description": "Blockchain network (e.g., ethereum, solana)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "mainnet": { "value": "mainnet" }, - "mainnet-beta": { "value": "mainnet-beta" }, - "devnet": { "value": "devnet" } + "mainnet": { + "value": "mainnet" + }, + "mainnet-beta": { + "value": "mainnet-beta" + }, + "devnet": { + "value": "devnet" + } }, "in": "query", "name": "network", @@ -459,7 +625,9 @@ "description": "Network name (e.g., mainnet, mainnet-beta)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "path", "name": "symbolOrAddress", "required": true, @@ -477,13 +645,21 @@ "token": { "type": "object", "properties": { - "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, + "chainId": { + "description": "The chain ID", + "type": "number", + "example": 1 + }, "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, - "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, + "symbol": { + "description": "The token symbol", + "type": "string", + "example": "USDC" + }, "address": { "description": "The token contract address", "type": "string", @@ -499,8 +675,12 @@ }, "required": ["name", "symbol", "address", "decimals"] }, - "chain": { "type": "string" }, - "network": { "type": "string" } + "chain": { + "type": "string" + }, + "network": { + "type": "string" + } }, "required": ["token", "chain", "network"] } @@ -516,12 +696,22 @@ "description": "Get token information with market data from GeckoTerminal by address", "parameters": [ { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, - "ethereum-mainnet": { "value": "ethereum-mainnet" }, - "ethereum-base": { "value": "ethereum-base" }, - "ethereum-polygon": { "value": "ethereum-polygon" } + "solana-mainnet-beta": { + "value": "solana-mainnet-beta" + }, + "ethereum-mainnet": { + "value": "ethereum-mainnet" + }, + "ethereum-base": { + "value": "ethereum-base" + }, + "ethereum-polygon": { + "value": "ethereum-polygon" + } }, "in": "query", "name": "chainNetwork", @@ -529,10 +719,16 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } + "So11111111111111111111111111111111111111112": { + "value": "So11111111111111111111111111111111111111112" + }, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { + "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + } }, "in": "path", "name": "address", @@ -548,9 +744,21 @@ "schema": { "type": "object", "properties": { - "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, - "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, - "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, + "chainId": { + "description": "The chain ID", + "type": "number", + "example": 1 + }, + "name": { + "description": "The full name of the token", + "type": "string", + "example": "USD Coin" + }, + "symbol": { + "description": "The token symbol", + "type": "string", + "example": "USDC" + }, "address": { "description": "The token contract address", "type": "string", @@ -578,19 +786,36 @@ "description": "List tokens from token lists with optional filtering", "parameters": [ { - "schema": { "type": "string" }, - "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, + "schema": { + "type": "string" + }, + "examples": { + "ethereum": { + "value": "ethereum" + }, + "solana": { + "value": "solana" + } + }, "in": "query", "name": "chain", "required": false, "description": "Blockchain network (e.g., ethereum, solana)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "mainnet": { "value": "mainnet" }, - "mainnet-beta": { "value": "mainnet-beta" }, - "devnet": { "value": "devnet" } + "mainnet": { + "value": "mainnet" + }, + "mainnet-beta": { + "value": "mainnet-beta" + }, + "devnet": { + "value": "devnet" + } }, "in": "query", "name": "network", @@ -598,8 +823,17 @@ "description": "Network name (e.g., mainnet, mainnet-beta)" }, { - "schema": { "type": "string" }, - "examples": { "USDC": { "value": "USDC" }, "USD": { "value": "USD" } }, + "schema": { + "type": "string" + }, + "examples": { + "USDC": { + "value": "USDC" + }, + "USD": { + "value": "USD" + } + }, "in": "query", "name": "search", "required": false, @@ -619,13 +853,21 @@ "items": { "type": "object", "properties": { - "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, + "chainId": { + "description": "The chain ID", + "type": "number", + "example": 1 + }, "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, - "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, + "symbol": { + "description": "The token symbol", + "type": "string", + "example": "USDC" + }, "address": { "description": "The token contract address", "type": "string", @@ -672,9 +914,21 @@ "token": { "type": "object", "properties": { - "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, - "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, - "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, + "chainId": { + "description": "The chain ID", + "type": "number", + "example": 1 + }, + "name": { + "description": "The full name of the token", + "type": "string", + "example": "USD Coin" + }, + "symbol": { + "description": "The token symbol", + "type": "string", + "example": "USDC" + }, "address": { "description": "The token contract address", "type": "string", @@ -704,7 +958,12 @@ "application/json": { "schema": { "type": "object", - "properties": { "message": { "description": "Success message", "type": "string" } }, + "properties": { + "message": { + "description": "Success message", + "type": "string" + } + }, "required": ["message"] } } @@ -719,12 +978,22 @@ "description": "Find token from GeckoTerminal and save it to the token list", "parameters": [ { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, - "ethereum-mainnet": { "value": "ethereum-mainnet" }, - "ethereum-base": { "value": "ethereum-base" }, - "ethereum-polygon": { "value": "ethereum-polygon" } + "solana-mainnet-beta": { + "value": "solana-mainnet-beta" + }, + "ethereum-mainnet": { + "value": "ethereum-mainnet" + }, + "ethereum-base": { + "value": "ethereum-base" + }, + "ethereum-polygon": { + "value": "ethereum-polygon" + } }, "in": "query", "name": "chainNetwork", @@ -732,10 +1001,16 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" } + "So11111111111111111111111111111111111111112": { + "value": "So11111111111111111111111111111111111111112" + }, + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48": { + "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + } }, "in": "path", "name": "address", @@ -751,17 +1026,27 @@ "schema": { "type": "object", "properties": { - "message": { "type": "string" }, + "message": { + "type": "string" + }, "token": { "type": "object", "properties": { - "chainId": { "description": "The chain ID", "type": "number", "example": 1 }, + "chainId": { + "description": "The chain ID", + "type": "number", + "example": 1 + }, "name": { "description": "The full name of the token", "type": "string", "example": "USD Coin" }, - "symbol": { "description": "The token symbol", "type": "string", "example": "USDC" }, + "symbol": { + "description": "The token symbol", + "type": "string", + "example": "USDC" + }, "address": { "description": "The token contract address", "type": "string", @@ -792,19 +1077,36 @@ "description": "Remove a token from a token list by address", "parameters": [ { - "schema": { "type": "string" }, - "examples": { "ethereum": { "value": "ethereum" }, "solana": { "value": "solana" } }, + "schema": { + "type": "string" + }, + "examples": { + "ethereum": { + "value": "ethereum" + }, + "solana": { + "value": "solana" + } + }, "in": "query", "name": "chain", "required": true, "description": "Blockchain network (e.g., ethereum, solana)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "mainnet": { "value": "mainnet" }, - "mainnet-beta": { "value": "mainnet-beta" }, - "devnet": { "value": "devnet" } + "mainnet": { + "value": "mainnet" + }, + "mainnet-beta": { + "value": "mainnet-beta" + }, + "devnet": { + "value": "devnet" + } }, "in": "query", "name": "network", @@ -812,7 +1114,9 @@ "description": "Network name (e.g., mainnet, mainnet-beta)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "path", "name": "address", "required": true, @@ -826,7 +1130,12 @@ "application/json": { "schema": { "type": "object", - "properties": { "message": { "description": "Success message", "type": "string" } }, + "properties": { + "message": { + "description": "Success message", + "type": "string" + } + }, "required": ["message"] } } @@ -841,36 +1150,75 @@ "description": "Get a specific pool by trading pair", "parameters": [ { - "schema": { "type": "string" }, - "examples": { "solana": { "value": "solana" }, "ethereum": { "value": "ethereum" } }, + "schema": { + "type": "string" + }, + "examples": { + "solana": { + "value": "solana" + }, + "ethereum": { + "value": "ethereum" + } + }, "in": "query", "name": "chain", "required": true, "description": "Blockchain chain (solana, ethereum)" }, { - "schema": { "default": "mainnet-beta", "type": "string" }, - "examples": { "mainnet-beta": { "value": "mainnet-beta" }, "mainnet": { "value": "mainnet" } }, + "schema": { + "default": "mainnet-beta", + "type": "string" + }, + "examples": { + "mainnet-beta": { + "value": "mainnet-beta" + }, + "mainnet": { + "value": "mainnet" + } + }, "in": "query", "name": "network", "required": true, "description": "Network name (mainnet, mainnet-beta, etc)" }, { - "schema": { "enum": ["amm", "clmm"], "type": "string" }, - "examples": { "amm": { "value": "amm" }, "clmm": { "value": "clmm" } }, + "schema": { + "enum": ["amm", "clmm"], + "type": "string" + }, + "examples": { + "amm": { + "value": "amm" + }, + "clmm": { + "value": "clmm" + } + }, "in": "query", "name": "type", "required": true, "description": "Pool type" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "raydium": { "value": "raydium" }, - "meteora": { "value": "meteora" }, - "uniswap": { "value": "uniswap" }, - "orca": { "value": "orca" } + "raydium": { + "value": "raydium" + }, + "meteora": { + "value": "meteora" + }, + "uniswap": { + "value": "uniswap" + }, + "orca": { + "value": "orca" + } }, "in": "query", "name": "connector", @@ -878,8 +1226,17 @@ "description": "Optional: filter by connector (raydium, meteora, uniswap, orca)" }, { - "schema": { "type": "string" }, - "examples": { "SOL-USDC": { "value": "SOL-USDC" }, "ETH-USDC": { "value": "ETH-USDC" } }, + "schema": { + "type": "string" + }, + "examples": { + "SOL-USDC": { + "value": "SOL-USDC" + }, + "ETH-USDC": { + "value": "ETH-USDC" + } + }, "in": "path", "name": "tradingPair", "required": true, @@ -905,18 +1262,32 @@ "type": "string", "example": "clmm" }, - "network": { "type": "string" }, - "baseSymbol": { "type": "string" }, - "quoteSymbol": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "address": { "type": "string" } - }, - "required": [ - "connector", - "type", - "network", + "network": { + "type": "string" + }, + "baseSymbol": { + "type": "string" + }, + "quoteSymbol": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "address": { + "type": "string" + } + }, + "required": [ + "connector", + "type", + "network", "baseSymbol", "quoteSymbol", "baseTokenAddress", @@ -931,7 +1302,16 @@ "404": { "description": "Default Response", "content": { - "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } } } } @@ -943,12 +1323,22 @@ "description": "Get detailed pool information by address from GeckoTerminal", "parameters": [ { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, - "ethereum-mainnet": { "value": "ethereum-mainnet" }, - "ethereum-base": { "value": "ethereum-base" }, - "ethereum-polygon": { "value": "ethereum-polygon" } + "solana-mainnet-beta": { + "value": "solana-mainnet-beta" + }, + "ethereum-mainnet": { + "value": "ethereum-mainnet" + }, + "ethereum-base": { + "value": "ethereum-base" + }, + "ethereum-polygon": { + "value": "ethereum-polygon" + } }, "in": "query", "name": "chainNetwork", @@ -956,12 +1346,16 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2": { "value": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" }, - "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" } + "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { + "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" + } }, "in": "path", "name": "address", @@ -988,13 +1382,27 @@ "type": "string", "example": "clmm" }, - "network": { "type": "string" }, - "baseSymbol": { "type": "string" }, - "quoteSymbol": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "address": { "type": "string" } + "network": { + "type": "string" + }, + "baseSymbol": { + "type": "string" + }, + "quoteSymbol": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "address": { + "type": "string" + } }, "required": [ "connector", @@ -1020,12 +1428,22 @@ "description": "Find pools for a token pair from GeckoTerminal", "parameters": [ { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, - "ethereum-mainnet": { "value": "ethereum-mainnet" }, - "ethereum-base": { "value": "ethereum-base" }, - "ethereum-polygon": { "value": "ethereum-polygon" } + "solana-mainnet-beta": { + "value": "solana-mainnet-beta" + }, + "ethereum-mainnet": { + "value": "ethereum-mainnet" + }, + "ethereum-base": { + "value": "ethereum-base" + }, + "ethereum-polygon": { + "value": "ethereum-polygon" + } }, "in": "query", "name": "chainNetwork", @@ -1033,14 +1451,28 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "raydium": { "value": "raydium" }, - "meteora": { "value": "meteora" }, - "uniswap": { "value": "uniswap" }, - "pancakeswap": { "value": "pancakeswap" }, - "pancakeswap-sol": { "value": "pancakeswap-sol" }, - "orca": { "value": "orca" } + "raydium": { + "value": "raydium" + }, + "meteora": { + "value": "meteora" + }, + "uniswap": { + "value": "uniswap" + }, + "pancakeswap": { + "value": "pancakeswap" + }, + "pancakeswap-sol": { + "value": "pancakeswap-sol" + }, + "orca": { + "value": "orca" + } }, "in": "query", "name": "connector", @@ -1048,19 +1480,38 @@ "description": "Filter by connector name (e.g., raydium, meteora, uniswap, pancakeswap, pancakeswap-sol)" }, { - "schema": { "enum": ["clmm", "amm"], "default": "clmm", "type": "string" }, - "examples": { "clmm": { "value": "clmm" }, "amm": { "value": "amm" } }, + "schema": { + "enum": ["clmm", "amm"], + "default": "clmm", + "type": "string" + }, + "examples": { + "clmm": { + "value": "clmm" + }, + "amm": { + "value": "amm" + } + }, "in": "query", "name": "type", "required": false, "description": "Filter by pool type: clmm (v3-style concentrated liquidity) or amm (v2-style)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "SOL": { "value": "SOL" }, - "So11111111111111111111111111111111111111112": { "value": "So11111111111111111111111111111111111111112" }, - "USDC": { "value": "USDC" } + "SOL": { + "value": "SOL" + }, + "So11111111111111111111111111111111111111112": { + "value": "So11111111111111111111111111111111111111112" + }, + "USDC": { + "value": "USDC" + } }, "in": "query", "name": "tokenA", @@ -1068,13 +1519,19 @@ "description": "First token symbol or contract address (optional - for filtering by token pair)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "USDC": { "value": "USDC" }, + "USDC": { + "value": "USDC" + }, "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": { "value": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" }, - "SOL": { "value": "SOL" } + "SOL": { + "value": "SOL" + } }, "in": "query", "name": "tokenB", @@ -1082,7 +1539,12 @@ "description": "Second token symbol or contract address (optional - for filtering by token pair)" }, { - "schema": { "minimum": 1, "maximum": 10, "default": 10, "type": "number" }, + "schema": { + "minimum": 1, + "maximum": 10, + "default": 10, + "type": "number" + }, "in": "query", "name": "pages", "required": false, @@ -1110,13 +1572,27 @@ "type": "string", "example": "clmm" }, - "network": { "type": "string" }, - "baseSymbol": { "type": "string" }, - "quoteSymbol": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "address": { "type": "string" } + "network": { + "type": "string" + }, + "baseSymbol": { + "type": "string" + }, + "quoteSymbol": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "address": { + "type": "string" + } }, "required": [ "connector", @@ -1143,20 +1619,39 @@ "description": "List all pools for a chain/network, optionally filtered by connector, type, or search term", "parameters": [ { - "schema": { "type": "string" }, - "examples": { "solana": { "value": "solana" }, "ethereum": { "value": "ethereum" } }, + "schema": { + "type": "string" + }, + "examples": { + "solana": { + "value": "solana" + }, + "ethereum": { + "value": "ethereum" + } + }, "in": "query", "name": "chain", "required": true, "description": "Blockchain chain (solana, ethereum)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "mainnet-beta": { "value": "mainnet-beta" }, - "mainnet": { "value": "mainnet" }, - "base": { "value": "base" }, - "arbitrum": { "value": "arbitrum" } + "mainnet-beta": { + "value": "mainnet-beta" + }, + "mainnet": { + "value": "mainnet" + }, + "base": { + "value": "base" + }, + "arbitrum": { + "value": "arbitrum" + } }, "in": "query", "name": "network", @@ -1164,12 +1659,22 @@ "description": "Network name (mainnet-beta, mainnet, base, etc)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "raydium": { "value": "raydium" }, - "meteora": { "value": "meteora" }, - "uniswap": { "value": "uniswap" }, - "orca": { "value": "orca" } + "raydium": { + "value": "raydium" + }, + "meteora": { + "value": "meteora" + }, + "uniswap": { + "value": "uniswap" + }, + "orca": { + "value": "orca" + } }, "in": "query", "name": "connector", @@ -1177,15 +1682,27 @@ "description": "Optional: filter by connector (raydium, meteora, uniswap, orca)" }, { - "schema": { "enum": ["clmm", "amm"], "type": "string" }, - "examples": { "clmm": { "value": "clmm" }, "amm": { "value": "amm" } }, + "schema": { + "enum": ["clmm", "amm"], + "type": "string" + }, + "examples": { + "clmm": { + "value": "clmm" + }, + "amm": { + "value": "amm" + } + }, "in": "query", "name": "type", "required": false, "description": "Optional: filter by pool type" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "query", "name": "search", "required": false, @@ -1213,13 +1730,27 @@ "type": "string", "example": "clmm" }, - "network": { "type": "string" }, - "baseSymbol": { "type": "string" }, - "quoteSymbol": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "address": { "type": "string" } + "network": { + "type": "string" + }, + "baseSymbol": { + "type": "string" + }, + "quoteSymbol": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "address": { + "type": "string" + } }, "required": [ "connector", @@ -1258,14 +1789,22 @@ "type": "string", "example": "raydium" }, - "type": { "description": "Pool type", "enum": ["clmm", "amm"], "type": "string", "example": "clmm" }, + "type": { + "description": "Pool type", + "enum": ["clmm", "amm"], + "type": "string", + "example": "clmm" + }, "network": { "description": "Network name (mainnet, mainnet-beta, etc)", "default": "mainnet-beta", "type": "string", "example": "mainnet-beta" }, - "address": { "description": "Pool contract address", "type": "string" }, + "address": { + "description": "Pool contract address", + "type": "string" + }, "baseSymbol": { "description": "Base token symbol (optional - fetched automatically if not provided)", "type": "string", @@ -1315,7 +1854,11 @@ "application/json": { "schema": { "type": "object", - "properties": { "message": { "type": "string" } }, + "properties": { + "message": { + "type": "string" + } + }, "required": ["message"] } } @@ -1324,7 +1867,16 @@ "400": { "description": "Default Response", "content": { - "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } } } } @@ -1336,12 +1888,22 @@ "description": "Find pool from GeckoTerminal and save it to the pool list. Auto-adds missing tokens.", "parameters": [ { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { - "solana-mainnet-beta": { "value": "solana-mainnet-beta" }, - "ethereum-mainnet": { "value": "ethereum-mainnet" }, - "ethereum-base": { "value": "ethereum-base" }, - "ethereum-polygon": { "value": "ethereum-polygon" } + "solana-mainnet-beta": { + "value": "solana-mainnet-beta" + }, + "ethereum-mainnet": { + "value": "ethereum-mainnet" + }, + "ethereum-base": { + "value": "ethereum-base" + }, + "ethereum-polygon": { + "value": "ethereum-polygon" + } }, "in": "query", "name": "chainNetwork", @@ -1349,12 +1911,16 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "examples": { "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2": { "value": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2" }, - "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" } + "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640": { + "value": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640" + } }, "in": "path", "name": "address", @@ -1370,7 +1936,9 @@ "schema": { "type": "object", "properties": { - "message": { "type": "string" }, + "message": { + "type": "string" + }, "pool": { "type": "object", "properties": { @@ -1385,13 +1953,27 @@ "type": "string", "example": "clmm" }, - "network": { "type": "string" }, - "baseSymbol": { "type": "string" }, - "quoteSymbol": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "address": { "type": "string" } + "network": { + "type": "string" + }, + "baseSymbol": { + "type": "string" + }, + "quoteSymbol": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "address": { + "type": "string" + } }, "required": [ "connector", @@ -1405,7 +1987,12 @@ "address" ] }, - "tokensAdded": { "type": "array", "items": { "type": "string" } } + "tokensAdded": { + "type": "array", + "items": { + "type": "string" + } + } }, "required": ["message", "pool"] } @@ -1421,23 +2008,43 @@ "description": "Remove a pool by address", "parameters": [ { - "schema": { "type": "string" }, - "examples": { "solana": { "value": "solana" }, "ethereum": { "value": "ethereum" } }, + "schema": { + "type": "string" + }, + "examples": { + "solana": { + "value": "solana" + }, + "ethereum": { + "value": "ethereum" + } + }, "in": "query", "name": "chain", "required": true, "description": "Blockchain chain (solana, ethereum)" }, { - "schema": { "type": "string" }, - "examples": { "mainnet": { "value": "mainnet" }, "mainnet-beta": { "value": "mainnet-beta" } }, + "schema": { + "type": "string" + }, + "examples": { + "mainnet": { + "value": "mainnet" + }, + "mainnet-beta": { + "value": "mainnet-beta" + } + }, "in": "query", "name": "network", "required": true, "description": "Network name (mainnet, mainnet-beta, etc)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "path", "name": "address", "required": true, @@ -1451,7 +2058,11 @@ "application/json": { "schema": { "type": "object", - "properties": { "message": { "type": "string" } }, + "properties": { + "message": { + "type": "string" + } + }, "required": ["message"] } } @@ -1460,7 +2071,16 @@ "404": { "description": "Default Response", "content": { - "application/json": { "schema": { "type": "object", "properties": { "message": { "type": "string" } } } } + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + } + } } } } @@ -1472,7 +2092,10 @@ "description": "Get a swap quote for any supported chain", "parameters": [ { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "schema": { + "default": "solana-mainnet-beta", + "type": "string" + }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -1480,7 +2103,9 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "jupiter/router", "in": "query", "name": "connector", @@ -1488,35 +2113,61 @@ "description": "Connector to use in format: connector/type (e.g., jupiter/router, raydium/amm, uniswap/clmm). If not provided, uses network's configured swapProvider" }, { - "schema": { "default": "SOL", "type": "string" }, + "schema": { + "default": "SOL", + "type": "string" + }, "in": "query", "name": "baseToken", "required": true, "description": "Symbol or address of the base token" }, { - "schema": { "default": "USDC", "type": "string" }, + "schema": { + "default": "USDC", + "type": "string" + }, "in": "query", "name": "quoteToken", "required": true, "description": "Symbol or address of the quote token" }, { - "schema": { "default": 1, "type": "number" }, + "schema": { + "default": 1, + "type": "number" + }, "in": "query", "name": "amount", "required": true, "description": "Amount to swap" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Side of the swap" }, { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "schema": { + "type": "string" + }, + "in": "query", + "name": "poolAddress", + "required": false, + "description": "Pin the swap to a specific pool. Only meaningful for amm/clmm providers, which trade against one pool; router providers choose their own route and reject it. Omit to resolve the pool from Gateway's configured pool list by token pair — which a pool that is not in that list (a freshly created one, an unlisted token) cannot be, so pass its address here." + }, + { + "schema": { + "minimum": 0, + "maximum": 100, + "type": "number" + }, "example": 1, "in": "query", "name": "slippagePct", @@ -1524,7 +2175,10 @@ "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct." }, { - "schema": { "default": true, "type": "boolean" }, + "schema": { + "default": true, + "type": "boolean" + }, "in": "query", "name": "approximateIfNoExactOut", "required": false, @@ -1539,23 +2193,53 @@ "schema": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, - "minAmountOut": { - "description": "Minimum amount of tokenOut that will be accepted", + "tokenIn": { + "description": "Address of the token being swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token being swapped to", + "type": "string" + }, + "amountIn": { + "description": "Amount of tokenIn to be swapped", "type": "number" }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, - "poolAddress": { "description": "Pool address for AMM/CLMM swaps", "type": "string" }, - "routePath": { "description": "Route path for router-based swaps", "type": "string" }, - "slippagePct": { "description": "Slippage tolerance percentage", "type": "number" } - }, - "required": [ - "tokenIn", + "amountOut": { + "description": "Expected amount of tokenOut to receive", + "type": "number" + }, + "price": { + "description": "Exchange rate between tokenIn and tokenOut", + "type": "number" + }, + "priceImpactPct": { + "description": "Estimated price impact percentage (0-100)", + "type": "number" + }, + "minAmountOut": { + "description": "Minimum amount of tokenOut that will be accepted", + "type": "number" + }, + "maxAmountIn": { + "description": "Maximum amount of tokenIn that will be spent", + "type": "number" + }, + "poolAddress": { + "description": "Pool address for AMM/CLMM swaps", + "type": "string" + }, + "routePath": { + "description": "Route path for router-based swaps", + "type": "string" + }, + "slippagePct": { + "description": "Slippage tolerance percentage", + "type": "number" + } + }, + "required": [ + "tokenIn", "tokenOut", "amountIn", "amountOut", @@ -1607,13 +2291,21 @@ "default": "USDC", "type": "string" }, - "amount": { "description": "Amount to swap", "default": 1, "type": "number" }, + "amount": { + "description": "Amount to swap", + "default": 1, + "type": "number" + }, "side": { "description": "Side of the swap", "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "poolAddress": { + "description": "Pin the swap to a specific pool. Only meaningful for amm/clmm providers, which trade against one pool; router providers choose their own route and reject it. Omit to resolve the pool from Gateway's configured pool list by token pair — which a pool that is not in that list (a freshly created one, an unlisted token) cannot be, so pass its address here.", + "type": "string" + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -1641,7 +2333,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -1649,11 +2344,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -1661,6 +2371,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -1700,7 +2414,10 @@ "description": "CLMM connector" }, { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "schema": { + "default": "solana-mainnet-beta", + "type": "string" + }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -1708,7 +2425,9 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", "in": "query", "name": "poolAddress", @@ -1716,7 +2435,12 @@ "description": "Pool contract address" }, { - "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "schema": { + "default": 0, + "minimum": 0, + "maximum": 401, + "type": "integer" + }, "in": "query", "name": "binCount", "required": false, @@ -1731,24 +2455,50 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" }, + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "binStep": { + "type": "number" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "activeBinId": { + "type": "number" + }, "bins": { "type": "array", "items": { "type": "object", "properties": { - "binId": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "binId": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"], "title": "BinLiquidity" @@ -1790,7 +2540,10 @@ "description": "CLMM connector" }, { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "schema": { + "default": "solana-mainnet-beta", + "type": "string" + }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -1798,7 +2551,9 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "", "in": "query", "name": "positionAddress", @@ -1814,19 +2569,45 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -1868,7 +2649,10 @@ "description": "CLMM connector" }, { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "schema": { + "default": "solana-mainnet-beta", + "type": "string" + }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -1876,7 +2660,10 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": true, @@ -1893,19 +2680,45 @@ "items": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -1949,7 +2762,10 @@ "description": "CLMM connector" }, { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "schema": { + "default": "solana-mainnet-beta", + "type": "string" + }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -1957,7 +2773,9 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 150, "in": "query", "name": "lowerPrice", @@ -1965,7 +2783,9 @@ "description": "Lower price bound for the position" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 250, "in": "query", "name": "upperPrice", @@ -1973,7 +2793,9 @@ "description": "Upper price bound for the position" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", "in": "query", "name": "poolAddress", @@ -1981,7 +2803,9 @@ "description": "Pool contract address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -1989,7 +2813,9 @@ "description": "Amount of base token to deposit" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -1997,7 +2823,11 @@ "description": "Amount of quote token to deposit" }, { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "type": "number" + }, "example": 1, "in": "query", "name": "slippagePct", @@ -2013,11 +2843,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" }, + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + }, "liquidity": {} }, "required": [ @@ -2114,16 +2954,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "positionAddress": { + "type": "string" + }, + "positionRent": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": [ "fee", @@ -2212,14 +3067,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -2295,14 +3161,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -2363,14 +3240,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -2431,17 +3319,34 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "positionRentRefunded": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": [ "fee", @@ -2489,8 +3394,12 @@ "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "baseToken": { "type": "string" }, - "quoteToken": { "type": "string" }, + "baseToken": { + "type": "string" + }, + "quoteToken": { + "type": "string" + }, "initialPrice": { "description": "Initial pool price as quote per base. If omitted, the current market price is fetched from the unified swap router so the pool opens on-market.", "type": "number" @@ -2522,14 +3431,30 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was initialized at (quote per base)", "type": "number" }, - "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + "data": { + "type": "object", + "properties": { + "fee": { + "type": "number" + } + }, + "required": ["fee"] + } }, "required": ["signature", "status", "poolAddress"] } @@ -2557,7 +3482,10 @@ "description": "AMM connector" }, { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "schema": { + "default": "solana-mainnet-beta", + "type": "string" + }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -2565,7 +3493,9 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "query", "name": "poolAddress", "required": true, @@ -2580,18 +3510,32 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } - }, - "required": [ - "address", - "baseTokenAddress", - "quoteTokenAddress", + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } + }, + "required": [ + "address", + "baseTokenAddress", + "quoteTokenAddress", "feePct", "price", "baseTokenAmount", @@ -2622,7 +3566,10 @@ "description": "AMM connector" }, { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "schema": { + "default": "solana-mainnet-beta", + "type": "string" + }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -2630,14 +3577,19 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "query", "name": "poolAddress", "required": true, "description": "Pool contract address" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": true, @@ -2652,14 +3604,30 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "walletAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "lpTokenAmount": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "price": { "type": "number" }, + "poolAddress": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "lpTokenAmount": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "price": { + "type": "number" + }, "positions": { "type": "array", "items": { @@ -2673,8 +3641,12 @@ "description": "Liquidity held by this position (LP units)", "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"], "title": "PositionDetail" @@ -2716,7 +3688,10 @@ "description": "AMM connector (only non-fungible-LP AMMs supported: meteora)" }, { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, + "schema": { + "default": "solana-mainnet-beta", + "type": "string" + }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -2724,7 +3699,10 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": true, @@ -2741,14 +3719,30 @@ "items": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "walletAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "lpTokenAmount": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "price": { "type": "number" }, + "poolAddress": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "lpTokenAmount": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "price": { + "type": "number" + }, "positions": { "type": "array", "items": { @@ -2762,8 +3756,12 @@ "description": "Liquidity held by this position (LP units)", "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] } @@ -2787,10 +3785,10 @@ } } }, - "/trading/amm/quote-swap": { + "/trading/amm/quote-liquidity": { "get": { "tags": ["/trading/amm"], - "description": "Get a swap quote against a specific AMM pool from any supported connector", + "description": "Quote amounts for adding liquidity to an AMM pool from any supported connector", "parameters": [ { "schema": { @@ -2804,107 +3802,11 @@ "required": true, "description": "AMM connector" }, - { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, - "example": "solana-mainnet-beta", - "in": "query", - "name": "chainNetwork", - "required": true, - "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" - }, - { - "schema": { "type": "string" }, - "in": "query", - "name": "poolAddress", - "required": true, - "description": "Pool contract address" - }, - { - "schema": { "type": "string" }, - "in": "query", - "name": "baseToken", - "required": true, - "description": "Base token symbol or address (determines swap direction)" - }, - { - "schema": { "type": "number" }, - "in": "query", - "name": "amount", - "required": true, - "description": "Amount denominated in the base token" - }, - { - "schema": { "enum": ["BUY", "SELL"], "type": "string" }, - "in": "query", - "name": "side", - "required": true, - "description": "Trade direction" - }, - { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, - "example": 1, - "in": "query", - "name": "slippagePct", - "required": false, - "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct." - } - ], - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } - }, - "required": [ - "poolAddress", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "minAmountOut", - "maxAmountIn", - "priceImpactPct" - ] - } - } - } - } - } - } - }, - "/trading/amm/quote-liquidity": { - "get": { - "tags": ["/trading/amm"], - "description": "Quote amounts for adding liquidity to an AMM pool from any supported connector", - "parameters": [ { "schema": { - "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], - "default": "meteora", + "default": "solana-mainnet-beta", "type": "string" }, - "example": "meteora", - "in": "query", - "name": "connector", - "required": true, - "description": "AMM connector" - }, - { - "schema": { "default": "solana-mainnet-beta", "type": "string" }, "example": "solana-mainnet-beta", "in": "query", "name": "chainNetwork", @@ -2912,28 +3814,38 @@ "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "query", "name": "poolAddress", "required": true, "description": "Pool contract address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "in": "query", "name": "baseTokenAmount", "required": true, "description": "Amount of base token to deposit" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "in": "query", "name": "quoteTokenAmount", "required": true, "description": "Amount of quote token to deposit" }, { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "type": "number" + }, "example": 1, "in": "query", "name": "slippagePct", @@ -2949,11 +3861,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" } + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + } }, "required": [ "baseLimited", @@ -2969,10 +3891,10 @@ } } }, - "/trading/amm/execute-swap": { + "/trading/amm/add-liquidity": { "post": { "tags": ["/trading/amm"], - "description": "Execute a swap against a specific AMM pool from any supported connector", + "description": "Add liquidity to an AMM pool from any supported connector", "requestBody": { "content": { "application/json": { @@ -2997,13 +3919,22 @@ "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "poolAddress": { "description": "Pool contract address", "type": "string" }, - "baseToken": { - "description": "Base token symbol or address (determines swap direction)", + "poolAddress": { + "description": "Pool contract address", + "type": "string" + }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number" + }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number" + }, + "positionAddress": { + "description": "meteora only (DAMM v2 positions are NFTs): add to this specific position. Omit to open a new position. Ignored by fungible-LP AMMs.", "type": "string" }, - "amount": { "description": "Amount denominated in the base token", "type": "number" }, - "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], "type": "string" }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -3012,7 +3943,14 @@ "example": 1 } }, - "required": ["connector", "chainNetwork", "walletAddress", "poolAddress", "baseToken", "amount", "side"] + "required": [ + "connector", + "chainNetwork", + "walletAddress", + "poolAddress", + "baseTokenAmount", + "quoteTokenAmount" + ] } } }, @@ -3026,28 +3964,27 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, - "required": [ - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "fee", - "baseTokenBalanceChange", - "quoteTokenBalanceChange" - ] + "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } }, "required": ["signature", "status"] @@ -3058,10 +3995,10 @@ } } }, - "/trading/amm/add-liquidity": { + "/trading/amm/remove-liquidity": { "post": { "tags": ["/trading/amm"], - "description": "Add liquidity to an AMM pool from any supported connector", + "description": "Remove liquidity from an AMM pool from any supported connector", "requestBody": { "content": { "application/json": { @@ -3086,13 +4023,22 @@ "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "poolAddress": { "description": "Pool contract address", "type": "string" }, - "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "poolAddress": { + "description": "Pool contract address", + "type": "string" + }, "positionAddress": { - "description": "meteora only (DAMM v2 positions are NFTs): add to this specific position. Omit to open a new position. Ignored by fungible-LP AMMs.", + "description": "Required for meteora (DAMM v2 positions are NFTs): the specific position to remove from. List positions with position-info or positions-owned. Ignored by fungible-LP AMMs.", "type": "string" }, + "percentageToRemove": { + "minimum": 0, + "maximum": 100, + "description": "Percentage of liquidity to remove", + "default": 100, + "type": "number", + "example": 100 + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -3101,14 +4047,7 @@ "example": 1 } }, - "required": [ - "connector", - "chainNetwork", - "walletAddress", - "poolAddress", - "baseTokenAmount", - "quoteTokenAmount" - ] + "required": ["connector", "chainNetwork", "walletAddress", "poolAddress", "percentageToRemove"] } } }, @@ -3122,16 +4061,27 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, - "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] + "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } }, "required": ["signature", "status"] @@ -3142,10 +4092,10 @@ } } }, - "/trading/amm/remove-liquidity": { + "/trading/amm/create-pool": { "post": { "tags": ["/trading/amm"], - "description": "Remove liquidity from an AMM pool from any supported connector", + "description": "Create and seed a new AMM pool across supported connectors (Meteora DAMM v2, Raydium CPMM, Uniswap V2, PancakeSwap V2)", "requestBody": { "content": { "application/json": { @@ -3166,90 +4116,7 @@ "example": "solana-mainnet-beta" }, "walletAddress": { - "description": "Wallet address", - "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", - "type": "string" - }, - "poolAddress": { "description": "Pool contract address", "type": "string" }, - "positionAddress": { - "description": "Required for meteora (DAMM v2 positions are NFTs): the specific position to remove from. List positions with position-info or positions-owned. Ignored by fungible-LP AMMs.", - "type": "string" - }, - "percentageToRemove": { - "minimum": 0, - "maximum": 100, - "description": "Percentage of liquidity to remove", - "default": 100, - "type": "number", - "example": 100 - }, - "slippagePct": { - "minimum": 0, - "maximum": 100, - "description": "Maximum acceptable slippage percentage. Defaults to the connector's configured slippagePct.", - "type": "number", - "example": 1 - } - }, - "required": ["connector", "chainNetwork", "walletAddress", "poolAddress", "percentageToRemove"] - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Default Response", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "data": { - "type": "object", - "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } - }, - "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] - } - }, - "required": ["signature", "status"] - } - } - } - } - } - } - }, - "/trading/amm/create-pool": { - "post": { - "tags": ["/trading/amm"], - "description": "Create and seed a new AMM pool across supported connectors (Meteora DAMM v2, Raydium CPMM, Uniswap V2, PancakeSwap V2)", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "connector": { - "description": "AMM connector", - "enum": ["meteora", "raydium", "uniswap", "pancakeswap"], - "default": "meteora", - "type": "string", - "example": "meteora" - }, - "chainNetwork": { - "description": "Chain and network in format: chain-network (e.g., solana-mainnet-beta, ethereum-mainnet)", - "default": "solana-mainnet-beta", - "type": "string", - "example": "solana-mainnet-beta" - }, - "walletAddress": { - "description": "Wallet address (pool creator + payer)", + "description": "Wallet address (pool creator + payer)", "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, @@ -3261,7 +4128,10 @@ "description": "Quote token symbol or address (becomes the pool quote)", "type": "string" }, - "baseTokenAmount": { "description": "Amount of base token to seed the pool with", "type": "number" }, + "baseTokenAmount": { + "description": "Amount of base token to seed the pool with", + "type": "number" + }, "quoteTokenAmount": { "description": "Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. If omitted (and no initialPrice), the price is fetched from the market.", "type": "number" @@ -3300,9 +4170,17 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was seeded at (quote per base)", "type": "number" @@ -3310,9 +4188,15 @@ "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -3331,7 +4215,11 @@ "description": "Get Solana network status", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, @@ -3346,13 +4234,27 @@ "schema": { "type": "object", "properties": { - "chain": { "type": "string" }, - "network": { "type": "string" }, - "rpcUrl": { "type": "string" }, - "rpcProvider": { "type": "string" }, - "currentBlockNumber": { "type": "number" }, - "nativeCurrency": { "type": "string" }, - "swapProvider": { "type": "string" } + "chain": { + "type": "string" + }, + "network": { + "type": "string" + }, + "rpcUrl": { + "type": "string" + }, + "rpcProvider": { + "type": "string" + }, + "currentBlockNumber": { + "type": "number" + }, + "nativeCurrency": { + "type": "string" + }, + "swapProvider": { + "type": "string" + } }, "required": [ "chain", @@ -3376,7 +4278,11 @@ "description": "Estimate priority fees for Solana transactions. Optionally pass addresses (program IDs, pools) for Helius-specific fee estimation.", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, @@ -3391,17 +4297,39 @@ "schema": { "type": "object", "properties": { - "feePerComputeUnit": { "type": "number" }, - "denomination": { "type": "string" }, - "computeUnits": { "type": "number" }, - "feeAsset": { "type": "string" }, - "fee": { "type": "number" }, - "timestamp": { "type": "number" }, - "gasType": { "type": "string" }, - "maxFeePerGas": { "type": "number" }, - "maxPriorityFeePerGas": { "type": "number" }, - "priorityFeeLevel": { "type": "string" }, - "priorityFeePerCUEstimate": { "type": "number" } + "feePerComputeUnit": { + "type": "number" + }, + "denomination": { + "type": "string" + }, + "computeUnits": { + "type": "number" + }, + "feeAsset": { + "type": "string" + }, + "fee": { + "type": "number" + }, + "timestamp": { + "type": "number" + }, + "gasType": { + "type": "string" + }, + "maxFeePerGas": { + "type": "number" + }, + "maxPriorityFeePerGas": { + "type": "number" + }, + "priorityFeeLevel": { + "type": "string" + }, + "priorityFeePerCUEstimate": { + "type": "number" + } }, "required": ["feePerComputeUnit", "denomination", "computeUnits", "feeAsset", "fee", "timestamp"] } @@ -3435,7 +4363,9 @@ "tokens": { "description": "A list of token symbols (SOL, USDC, BONK) from the network's token list. Only tokens in the token list will be returned. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of SOL).", "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "example": ["SOL", "USDC", "BONK"] } } @@ -3450,11 +4380,24 @@ "application/json": { "schema": { "type": "object", - "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, + "properties": { + "balances": { + "type": "object", + "additionalProperties": { + "type": "number" + } + } + }, "required": ["balances"], "description": "Token balances for the specified address (only tokens in token list)" }, - "example": { "balances": { "SOL": 1.5, "USDC": 100, "BONK": 50000 } } + "example": { + "balances": { + "SOL": 1.5, + "USDC": 100, + "BONK": 50000 + } + } } } } @@ -3497,21 +4440,58 @@ "schema": { "type": "object", "properties": { - "currentBlock": { "type": "number" }, - "signature": { "type": "string" }, - "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "currentBlock": { + "type": "number" + }, + "signature": { + "type": "string" + }, + "txBlock": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, "txStatus": { "description": "Transaction status: 1 = confirmed, 0 = pending, -1 = failed, -2 = not found (unknown to the chain: never received or dropped; on Solana this is terminal once the transaction blockhash expires)", "type": "number" }, - "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, + "fee": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, "error": { "anyOf": [ - { "description": "Error info if failed: \"TYPE (code): message\"", "type": "string" }, - { "type": "null" } + { + "description": "Error info if failed: \"TYPE (code): message\"", + "type": "string" + }, + { + "type": "null" + } ] }, - "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] } + "txData": { + "anyOf": [ + { + "type": "object", + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } }, "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "error", "txData"] } @@ -3562,16 +4542,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "string" }, - "amount": { "type": "string" }, - "wrappedAddress": { "type": "string" }, - "nativeToken": { "type": "string" }, - "wrappedToken": { "type": "string" } + "fee": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "wrappedAddress": { + "type": "string" + }, + "nativeToken": { + "type": "string" + }, + "wrappedToken": { + "type": "string" + } }, "required": ["fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] } @@ -3623,16 +4618,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "string" }, - "amount": { "type": "string" }, - "wrappedAddress": { "type": "string" }, - "nativeToken": { "type": "string" }, - "wrappedToken": { "type": "string" } + "fee": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "wrappedAddress": { + "type": "string" + }, + "nativeToken": { + "type": "string" + }, + "wrappedToken": { + "type": "string" + } }, "required": ["fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] } @@ -3683,13 +4693,27 @@ "schema": { "type": "object", "properties": { - "chain": { "type": "string" }, - "network": { "type": "string" }, - "rpcUrl": { "type": "string" }, - "rpcProvider": { "type": "string" }, - "currentBlockNumber": { "type": "number" }, - "nativeCurrency": { "type": "string" }, - "swapProvider": { "type": "string" } + "chain": { + "type": "string" + }, + "network": { + "type": "string" + }, + "rpcUrl": { + "type": "string" + }, + "rpcProvider": { + "type": "string" + }, + "currentBlockNumber": { + "type": "number" + }, + "nativeCurrency": { + "type": "string" + }, + "swapProvider": { + "type": "string" + } }, "required": [ "chain", @@ -3745,17 +4769,39 @@ "schema": { "type": "object", "properties": { - "feePerComputeUnit": { "type": "number" }, - "denomination": { "type": "string" }, - "computeUnits": { "type": "number" }, - "feeAsset": { "type": "string" }, - "fee": { "type": "number" }, - "timestamp": { "type": "number" }, - "gasType": { "type": "string" }, - "maxFeePerGas": { "type": "number" }, - "maxPriorityFeePerGas": { "type": "number" }, - "priorityFeeLevel": { "type": "string" }, - "priorityFeePerCUEstimate": { "type": "number" } + "feePerComputeUnit": { + "type": "number" + }, + "denomination": { + "type": "string" + }, + "computeUnits": { + "type": "number" + }, + "feeAsset": { + "type": "string" + }, + "fee": { + "type": "number" + }, + "timestamp": { + "type": "number" + }, + "gasType": { + "type": "string" + }, + "maxFeePerGas": { + "type": "number" + }, + "maxPriorityFeePerGas": { + "type": "number" + }, + "priorityFeeLevel": { + "type": "string" + }, + "priorityFeePerCUEstimate": { + "type": "number" + } }, "required": ["feePerComputeUnit", "denomination", "computeUnits", "feeAsset", "fee", "timestamp"] } @@ -3802,7 +4848,9 @@ "tokens": { "description": "A list of token symbols (ETH, USDC, WETH) or token addresses. Both formats are accepted and will be automatically detected. An empty array is treated the same as if the parameter was not provided, returning only non-zero balances (with the exception of ETH).", "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "example": ["ETH", "USDC", "WETH"] } } @@ -3817,7 +4865,14 @@ "application/json": { "schema": { "type": "object", - "properties": { "balances": { "type": "object", "additionalProperties": { "type": "number" } } }, + "properties": { + "balances": { + "type": "object", + "additionalProperties": { + "type": "number" + } + } + }, "required": ["balances"] } } @@ -3875,26 +4930,63 @@ "schema": { "type": "object", "properties": { - "currentBlock": { "type": "number" }, - "signature": { "type": "string" }, - "txBlock": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, - "txStatus": { - "description": "Transaction status: 1 = confirmed, 0 = pending, -1 = failed, -2 = not found (unknown to the chain: never received or dropped; on Solana this is terminal once the transaction blockhash expires)", + "currentBlock": { "type": "number" }, - "fee": { "anyOf": [{ "type": "number" }, { "type": "null" }] }, - "error": { + "signature": { + "type": "string" + }, + "txBlock": { "anyOf": [ - { "description": "Error info if failed: \"TYPE (code): message\"", "type": "string" }, - { "type": "null" } + { + "type": "number" + }, + { + "type": "null" + } ] }, - "txData": { "anyOf": [{ "type": "object", "additionalProperties": {} }, { "type": "null" }] } - }, - "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "error", "txData"] - } - } - } + "txStatus": { + "description": "Transaction status: 1 = confirmed, 0 = pending, -1 = failed, -2 = not found (unknown to the chain: never received or dropped; on Solana this is terminal once the transaction blockhash expires)", + "type": "number" + }, + "fee": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "error": { + "anyOf": [ + { + "description": "Error info if failed: \"TYPE (code): message\"", + "type": "string" + }, + { + "type": "null" + } + ] + }, + "txData": { + "anyOf": [ + { + "type": "object", + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "required": ["currentBlock", "signature", "txBlock", "txStatus", "fee", "error", "txData"] + } + } + } } } } @@ -3941,7 +5033,9 @@ "tokens": { "description": "Array of token symbols or addresses", "type": "array", - "items": { "type": "string" }, + "items": { + "type": "string" + }, "example": ["USDC", "WETH"] } }, @@ -3959,8 +5053,15 @@ "schema": { "type": "object", "properties": { - "spender": { "type": "string" }, - "approvals": { "type": "object", "additionalProperties": { "type": "string" } } + "spender": { + "type": "string" + }, + "approvals": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } }, "required": ["spender", "approvals"] } @@ -4009,7 +5110,11 @@ "type": "string", "example": "uniswap/router" }, - "token": { "description": "Token symbol or address", "type": "string", "example": "USDC" }, + "token": { + "description": "Token symbol or address", + "type": "string", + "example": "USDC" + }, "amount": { "description": "The amount to approve. If not provided, defaults to maximum amount (unlimited approval).", "default": "", @@ -4030,16 +5135,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenAddress": { "type": "string" }, - "spender": { "type": "string" }, - "amount": { "type": "string" }, - "nonce": { "type": "number" }, - "fee": { "type": "string" } + "tokenAddress": { + "type": "string" + }, + "spender": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "nonce": { + "type": "number" + }, + "fee": { + "type": "string" + } }, "required": ["tokenAddress", "spender", "amount", "nonce", "fee"] } @@ -4106,17 +5226,34 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "nonce": { "type": "number" }, - "fee": { "type": "string" }, - "amount": { "type": "string" }, - "wrappedAddress": { "type": "string" }, - "nativeToken": { "type": "string" }, - "wrappedToken": { "type": "string" } + "nonce": { + "type": "number" + }, + "fee": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "wrappedAddress": { + "type": "string" + }, + "nativeToken": { + "type": "string" + }, + "wrappedToken": { + "type": "string" + } }, "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] } @@ -4183,17 +5320,34 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "nonce": { "type": "number" }, - "fee": { "type": "string" }, - "amount": { "type": "string" }, - "wrappedAddress": { "type": "string" }, - "nativeToken": { "type": "string" }, - "wrappedToken": { "type": "string" } + "nonce": { + "type": "number" + }, + "fee": { + "type": "string" + }, + "amount": { + "type": "string" + }, + "wrappedAddress": { + "type": "string" + }, + "nativeToken": { + "type": "string" + }, + "wrappedToken": { + "type": "string" + } }, "required": ["nonce", "fee", "amount", "wrappedAddress", "nativeToken", "wrappedToken"] } @@ -4212,14 +5366,20 @@ "description": "Get an executable swap quote from Jupiter", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -4227,7 +5387,9 @@ "description": "Solana token symbol or address to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -4235,7 +5397,9 @@ "description": "The other Solana token symbol or address in the pair" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.1, "in": "query", "name": "amount", @@ -4243,21 +5407,33 @@ "description": "Amount of base token to trade" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 1, + "type": "number" + }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { "default": true, "type": "boolean" }, + "schema": { + "default": true, + "type": "boolean" + }, "in": "query", "name": "approximateIfNoExactOut", "required": false, @@ -4272,40 +5448,93 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "quoteId": { + "description": "Unique identifier for this quote", + "type": "string" + }, + "tokenIn": { + "description": "Address of the token being swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token being swapped to", + "type": "string" + }, + "amountIn": { + "description": "Amount of tokenIn to be swapped", + "type": "number" + }, + "amountOut": { + "description": "Expected amount of tokenOut to receive", + "type": "number" + }, + "price": { + "description": "Exchange rate between tokenIn and tokenOut", + "type": "number" + }, + "priceImpactPct": { + "description": "Estimated price impact percentage (0-100)", + "type": "number" + }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "maxAmountIn": { + "description": "Maximum amount of tokenIn that will be spent", + "type": "number" + }, "quoteResponse": { "type": "object", "properties": { - "inputMint": { "description": "Solana mint address of input token", "type": "string" }, - "inAmount": { "description": "Input amount in token decimals", "type": "string" }, - "outputMint": { "description": "Solana mint address of output token", "type": "string" }, - "outAmount": { "description": "Expected output amount in token decimals", "type": "string" }, + "inputMint": { + "description": "Solana mint address of input token", + "type": "string" + }, + "inAmount": { + "description": "Input amount in token decimals", + "type": "string" + }, + "outputMint": { + "description": "Solana mint address of output token", + "type": "string" + }, + "outAmount": { + "description": "Expected output amount in token decimals", + "type": "string" + }, "otherAmountThreshold": { "description": "Minimum output amount based on slippage", "type": "string" }, - "swapMode": { "description": "Swap mode used (ExactIn or ExactOut)", "type": "string" }, - "slippageBps": { "description": "Slippage in basis points", "type": "number" }, - "platformFee": { "description": "Platform fee information if applicable" }, - "priceImpactPct": { "description": "Estimated price impact percentage", "type": "string" }, + "swapMode": { + "description": "Swap mode used (ExactIn or ExactOut)", + "type": "string" + }, + "slippageBps": { + "description": "Slippage in basis points", + "type": "number" + }, + "platformFee": { + "description": "Platform fee information if applicable" + }, + "priceImpactPct": { + "description": "Estimated price impact percentage", + "type": "string" + }, "routePlan": { "description": "Detailed routing plan through various markets", "type": "array", "items": {} }, - "contextSlot": { "description": "Solana slot used for quote calculation", "type": "number" }, - "timeTaken": { "description": "Time taken to generate quote in milliseconds", "type": "number" } + "contextSlot": { + "description": "Solana slot used for quote calculation", + "type": "number" + }, + "timeTaken": { + "description": "Time taken to generate quote in milliseconds", + "type": "number" + } }, "required": [ "inputMint", @@ -4384,7 +5613,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -4392,11 +5624,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -4404,6 +5651,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -4456,7 +5707,11 @@ "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 0.1 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -4490,7 +5745,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -4498,11 +5756,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -4510,6 +5783,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -4537,14 +5814,20 @@ "description": "Get an executable swap quote from DFlow", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -4552,7 +5835,9 @@ "description": "Solana token symbol or address to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -4560,7 +5845,9 @@ "description": "The other Solana token symbol or address in the pair" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.1, "in": "query", "name": "amount", @@ -4568,21 +5855,33 @@ "description": "Amount of base token to trade" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 1, + "type": "number" + }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { "default": true, "type": "boolean" }, + "schema": { + "default": true, + "type": "boolean" + }, "in": "query", "name": "approximateIfNoExactOut", "required": false, @@ -4597,18 +5896,42 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "quoteId": { + "description": "Unique identifier for this quote", + "type": "string" + }, + "tokenIn": { + "description": "Address of the token being swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token being swapped to", + "type": "string" + }, + "amountIn": { + "description": "Amount of tokenIn to be swapped", + "type": "number" + }, + "amountOut": { + "description": "Expected amount of tokenOut to receive", + "type": "number" + }, + "price": { + "description": "Exchange rate between tokenIn and tokenOut", + "type": "number" + }, + "priceImpactPct": { + "description": "Estimated price impact percentage (0-100)", + "type": "number" + }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "maxAmountIn": { + "description": "Maximum amount of tokenIn that will be spent", + "type": "number" + }, "approximation": { "description": "True when a BUY was approximated via a sell-leg ExactIn quote (DFlow is ExactIn-only); amountOut is an estimate", "type": "boolean" @@ -4677,7 +6000,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -4685,11 +6011,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -4697,6 +6038,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -4749,7 +6094,11 @@ "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 0.1 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -4783,7 +6132,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -4791,11 +6143,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -4803,6 +6170,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -4830,14 +6201,20 @@ "description": "Get an executable swap quote from the OKX DEX aggregator", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -4845,7 +6222,9 @@ "description": "Solana token symbol or address to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -4853,7 +6232,9 @@ "description": "The other Solana token symbol or address in the pair" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.1, "in": "query", "name": "amount", @@ -4861,21 +6242,33 @@ "description": "Amount of base token to trade" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, - "in": "query", + "schema": { + "minimum": 0, + "maximum": 100, + "default": 1, + "type": "number" + }, + "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { "default": true, "type": "boolean" }, + "schema": { + "default": true, + "type": "boolean" + }, "in": "query", "name": "approximateIfNoExactOut", "required": false, @@ -4890,18 +6283,42 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "quoteId": { + "description": "Unique identifier for this quote", + "type": "string" + }, + "tokenIn": { + "description": "Address of the token being swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token being swapped to", + "type": "string" + }, + "amountIn": { + "description": "Amount of tokenIn to be swapped", + "type": "number" + }, + "amountOut": { + "description": "Expected amount of tokenOut to receive", + "type": "number" + }, + "price": { + "description": "Exchange rate between tokenIn and tokenOut", + "type": "number" + }, + "priceImpactPct": { + "description": "Estimated price impact percentage (0-100)", + "type": "number" + }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "maxAmountIn": { + "description": "Maximum amount of tokenIn that will be spent", + "type": "number" + }, "approximation": { "description": "True when a BUY was approximated via a sell-leg exactIn quote because exactOut was unavailable; amountOut is an estimate", "type": "boolean" @@ -4970,7 +6387,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -4978,11 +6398,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -4990,6 +6425,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -5042,7 +6481,11 @@ "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 0.1 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -5076,7 +6519,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -5084,11 +6530,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -5096,6 +6557,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -5123,14 +6588,20 @@ "description": "Get an executable swap quote from Titan (DART)", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -5138,7 +6609,9 @@ "description": "Solana token symbol or address to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -5146,7 +6619,9 @@ "description": "The other Solana token symbol or address in the pair" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.1, "in": "query", "name": "amount", @@ -5154,28 +6629,43 @@ "description": "Amount of base token to trade" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 1, + "type": "number" + }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { "default": true, "type": "boolean" }, + "schema": { + "default": true, + "type": "boolean" + }, "in": "query", "name": "approximateIfNoExactOut", "required": false, "description": "For BUY orders when the router has no ExactOut route: approximate via a sell-leg ExactIn quote instead of failing" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": false, @@ -5190,12 +6680,30 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, + "quoteId": { + "description": "Unique identifier for this quote", + "type": "string" + }, + "tokenIn": { + "description": "Address of the token being swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token being swapped to", + "type": "string" + }, + "amountIn": { + "description": "Amount of tokenIn to be swapped", + "type": "number" + }, + "amountOut": { + "description": "Expected amount of tokenOut to receive", + "type": "number" + }, + "price": { + "description": "Exchange rate between tokenIn and tokenOut", + "type": "number" + }, "priceImpactPct": { "description": "Estimated price impact percentage (0-100); Titan DART does not report price impact, so this is 0", "type": "number" @@ -5204,7 +6712,10 @@ "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "maxAmountIn": { + "description": "Maximum amount of tokenIn that will be spent", + "type": "number" + }, "approximation": { "description": "True when a BUY was approximated via a sell-leg ExactIn quote (Titan DART is ExactIn-only); amountOut is an estimate", "type": "boolean" @@ -5274,7 +6785,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -5282,11 +6796,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -5294,6 +6823,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -5346,7 +6879,11 @@ "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.1 }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 0.1 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -5380,7 +6917,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -5388,11 +6928,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -5400,6 +6955,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -5427,14 +6986,22 @@ "description": "Fetch Meteora pools from API with search and sorting", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "minimum": 0, "default": 0, "type": "number" }, + "schema": { + "minimum": 0, + "default": 0, + "type": "number" + }, "example": 0, "in": "query", "name": "page", @@ -5442,7 +7009,12 @@ "description": "Page number (0-based)" }, { - "schema": { "minimum": 1, "maximum": 1000, "default": 50, "type": "number" }, + "schema": { + "minimum": 1, + "maximum": 1000, + "default": 50, + "type": "number" + }, "example": 50, "in": "query", "name": "limit", @@ -5450,19 +7022,40 @@ "description": "Maximum number of pools to return (max 1000)" }, { - "schema": { "type": "string" }, - "examples": { "SOL": { "value": "SOL" }, "USDC": { "value": "USDC" }, "SOL-USDC": { "value": "SOL-USDC" } }, + "schema": { + "type": "string" + }, + "examples": { + "SOL": { + "value": "SOL" + }, + "USDC": { + "value": "USDC" + }, + "SOL-USDC": { + "value": "SOL-USDC" + } + }, "in": "query", "name": "query", "required": false, "description": "Search query to match pools by name, tokens, or address" }, { - "schema": { "default": "volume_24h:desc", "type": "string" }, + "schema": { + "default": "volume_24h:desc", + "type": "string" + }, "examples": { - "volume_24h:desc": { "value": "volume_24h:desc" }, - "tvl:desc": { "value": "tvl:desc" }, - "apr:desc": { "value": "apr:desc" } + "volume_24h:desc": { + "value": "volume_24h:desc" + }, + "tvl:desc": { + "value": "tvl:desc" + }, + "apr:desc": { + "value": "apr:desc" + } }, "in": "query", "name": "sortBy", @@ -5470,7 +7063,10 @@ "description": "Sort by field (volume, fees, tvl, apr) with optional time window" }, { - "schema": { "default": true, "type": "boolean" }, + "schema": { + "default": true, + "type": "boolean" + }, "in": "query", "name": "includeUnverified", "required": false, @@ -5490,20 +7086,62 @@ "items": { "type": "object", "properties": { - "address": { "description": "Pool address", "type": "string" }, - "name": { "description": "Pool name (e.g., SOL-USDC)", "type": "string" }, - "baseTokenAddress": { "description": "Base token address", "type": "string" }, - "baseTokenSymbol": { "description": "Base token symbol", "type": "string" }, - "quoteTokenAddress": { "description": "Quote token address", "type": "string" }, - "quoteTokenSymbol": { "description": "Quote token symbol", "type": "string" }, - "binStep": { "description": "Bin step / tick spacing", "type": "number" }, - "baseFee": { "description": "Base fee percentage", "type": "number" }, - "price": { "description": "Current price", "type": "number" }, - "tvl": { "description": "Total value locked in USD", "type": "number" }, - "apr": { "description": "Annual percentage rate", "type": "number" }, - "apy": { "description": "Annual percentage yield", "type": "number" }, - "volume24h": { "description": "24-hour trading volume", "type": "number" }, - "fees24h": { "description": "24-hour fees collected", "type": "number" } + "address": { + "description": "Pool address", + "type": "string" + }, + "name": { + "description": "Pool name (e.g., SOL-USDC)", + "type": "string" + }, + "baseTokenAddress": { + "description": "Base token address", + "type": "string" + }, + "baseTokenSymbol": { + "description": "Base token symbol", + "type": "string" + }, + "quoteTokenAddress": { + "description": "Quote token address", + "type": "string" + }, + "quoteTokenSymbol": { + "description": "Quote token symbol", + "type": "string" + }, + "binStep": { + "description": "Bin step / tick spacing", + "type": "number" + }, + "baseFee": { + "description": "Base fee percentage", + "type": "number" + }, + "price": { + "description": "Current price", + "type": "number" + }, + "tvl": { + "description": "Total value locked in USD", + "type": "number" + }, + "apr": { + "description": "Annual percentage rate", + "type": "number" + }, + "apy": { + "description": "Annual percentage yield", + "type": "number" + }, + "volume24h": { + "description": "24-hour trading volume", + "type": "number" + }, + "fees24h": { + "description": "24-hour fees collected", + "type": "number" + } }, "required": [ "address", @@ -5520,9 +7158,18 @@ "title": "PoolListItem" } }, - "total": { "description": "Total number of matching pools", "type": "number" }, - "page": { "description": "Current page number", "type": "number" }, - "pageSize": { "description": "Number of pools per page", "type": "number" } + "total": { + "description": "Total number of matching pools", + "type": "number" + }, + "page": { + "description": "Current page number", + "type": "number" + }, + "pageSize": { + "description": "Number of pools per page", + "type": "number" + } }, "required": ["pools", "total", "page", "pageSize"] } @@ -5554,8 +7201,16 @@ "type": "string", "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, - "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, + "baseToken": { + "description": "Base token symbol or address", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "Quote token symbol or address", + "type": "string", + "example": "USDC" + }, "initialPrice": { "description": "Initial price as quote per base (e.g. USDC per SOL). Encodes the pool active bin.", "type": "number", @@ -5586,14 +7241,30 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was initialized at (quote per base)", "type": "number" }, - "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + "data": { + "type": "object", + "properties": { + "fee": { + "type": "number" + } + }, + "required": ["fee"] + } }, "required": ["signature", "status", "poolAddress"] } @@ -5609,14 +7280,20 @@ "description": "Get pool information for a Meteora pool", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", "in": "query", "name": "poolAddress", @@ -5632,31 +7309,63 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" }, + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "binStep": { + "type": "number" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "activeBinId": { + "type": "number" + }, "bins": { "type": "array", "items": { "type": "object", "properties": { - "binId": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "binId": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] } }, - "dynamicFeePct": { "type": "number" }, - "minBinId": { "type": "number" }, - "maxBinId": { "type": "number" } + "dynamicFeePct": { + "type": "number" + }, + "minBinId": { + "type": "number" + }, + "maxBinId": { + "type": "number" + } }, "required": [ "address", @@ -5684,14 +7393,20 @@ "description": "Retrieve all positions owned by a user's wallet across all Meteora pools", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", @@ -5709,27 +7424,53 @@ "items": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } - }, - "required": [ - "address", - "poolAddress", - "baseTokenAddress", - "quoteTokenAddress", - "baseTokenAmount", - "quoteTokenAmount", + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } + }, + "required": [ + "address", + "poolAddress", + "baseTokenAddress", + "quoteTokenAddress", + "baseTokenAmount", + "quoteTokenAmount", "baseFeeAmount", "quoteFeeAmount", "lowerBinId", @@ -5752,14 +7493,20 @@ "description": "Get details for a specific Meteora position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "", "in": "query", "name": "positionAddress", @@ -5775,19 +7522,45 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -5817,14 +7590,20 @@ "description": "Quote amounts for a new Meteora CLMM position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 150, "in": "query", "name": "lowerPrice", @@ -5832,7 +7611,9 @@ "description": "Lower price bound for the position" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 250, "in": "query", "name": "upperPrice", @@ -5840,7 +7621,9 @@ "description": "Upper price bound for the position" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", "in": "query", "name": "poolAddress", @@ -5848,7 +7631,9 @@ "description": "Meteora DLMM pool address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -5856,7 +7641,9 @@ "description": "Amount of base token to deposit" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -5864,7 +7651,12 @@ "description": "Amount of quote token to deposit" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -5872,7 +7664,10 @@ "description": "Maximum acceptable slippage percentage" }, { - "schema": { "enum": [0, 1, 2], "type": "number" }, + "schema": { + "enum": [0, 1, 2], + "type": "number" + }, "example": 0, "in": "query", "name": "strategyType", @@ -5888,11 +7683,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" }, + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + }, "liquidity": {} }, "required": [ @@ -5915,14 +7720,20 @@ "description": "Get swap quote for Meteora CLMM", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3", "in": "query", "name": "poolAddress", @@ -5930,7 +7741,9 @@ "description": "Meteora DLMM pool address (optional - can be looked up from baseToken and quoteToken)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -5938,7 +7751,9 @@ "description": "Token to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -5946,7 +7761,9 @@ "description": "The other token in the pair (optional - required if poolAddress not provided)" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "amount", @@ -5954,7 +7771,11 @@ "description": "Amount to swap" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "example": "SELL", "in": "query", "name": "side", @@ -5962,7 +7783,12 @@ "description": "Trade direction" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -5978,16 +7804,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -6034,13 +7880,21 @@ "type": "string", "example": "2sf5NYcY4zUPXUSmG6f66mskb24t5F8S11pC1Nz5nQT3" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "baseToken": { + "description": "Base token symbol or address", + "type": "string", + "example": "SOL" + }, "quoteToken": { "description": "Quote token symbol or address (optional - required if poolAddress not provided)", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, + "amount": { + "description": "Amount to swap", + "type": "number", + "example": 0.01 + }, "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], @@ -6071,18 +7925,41 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "fee": { + "type": "number" + }, + "baseTokenBalanceChange": { + "type": "number" + }, + "quoteTokenBalanceChange": { + "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" + } }, "required": [ "tokenIn", @@ -6179,16 +8056,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "positionAddress": { + "type": "string" + }, + "positionRent": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": [ "fee", @@ -6273,14 +8165,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -6343,14 +8246,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -6405,14 +8319,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -6467,17 +8392,34 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "positionRentRefunded": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": [ "fee", @@ -6503,14 +8445,20 @@ "description": "Get AMM pool information from Meteora DAMM v2", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv", "in": "query", "name": "poolAddress", @@ -6526,13 +8474,27 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": [ "address", @@ -6556,14 +8518,20 @@ "description": "Get the wallet's aggregated liquidity in a Meteora DAMM v2 pool. DAMM v2 positions are NFTs; amounts sum across all of the wallet positions in the pool.", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv", "in": "query", "name": "poolAddress", @@ -6571,7 +8539,10 @@ "description": "Meteora DAMM v2 pool address" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": false, @@ -6586,17 +8557,33 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "walletAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "lpTokenAmount": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "price": { "type": "number" }, - "positions": { - "type": "array", - "items": { + "poolAddress": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "lpTokenAmount": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "price": { + "type": "number" + }, + "positions": { + "type": "array", + "items": { "type": "object", "properties": { "positionAddress": { @@ -6607,8 +8594,12 @@ "description": "Liquidity held by this position (LP units)", "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] } @@ -6637,14 +8628,20 @@ "description": "List all of a wallet's DAMM v2 positions across all Meteora AMM pools", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", @@ -6662,14 +8659,30 @@ "items": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "walletAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "lpTokenAmount": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "price": { "type": "number" }, + "poolAddress": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "lpTokenAmount": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "price": { + "type": "number" + }, "positions": { "type": "array", "items": { @@ -6683,8 +8696,12 @@ "description": "Liquidity held by this position (LP units)", "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] } @@ -6714,14 +8731,20 @@ "description": "Get a swap quote for a Meteora DAMM v2 pool", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv", "in": "query", "name": "poolAddress", @@ -6729,7 +8752,9 @@ "description": "Meteora DAMM v2 pool address" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -6737,7 +8762,9 @@ "description": "Token to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -6745,7 +8772,9 @@ "description": "The other token in the pair (optional - resolved from the pool if omitted)" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "amount", @@ -6753,14 +8782,23 @@ "description": "Amount to swap (denominated in the base token)" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -6776,16 +8814,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -6811,14 +8869,20 @@ "description": "Quote amounts for adding liquidity to a Meteora DAMM v2 pool", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv", "in": "query", "name": "poolAddress", @@ -6826,7 +8890,9 @@ "description": "Meteora DAMM v2 pool address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -6834,7 +8900,9 @@ "description": "Amount of base token to add" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -6842,7 +8910,12 @@ "description": "Amount of quote token to add" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -6858,11 +8931,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" } + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + } }, "required": [ "baseLimited", @@ -6904,7 +8987,11 @@ "type": "string", "example": "FH6mP2MUobhDnLERp9z5yv5t2zMUA9WDNXPixpbvYKMv" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "baseToken": { + "description": "Base token symbol or address", + "type": "string", + "example": "SOL" + }, "quoteToken": { "description": "The other token in the pair (optional - resolved from the pool if omitted)", "type": "string", @@ -6944,18 +9031,41 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "fee": { + "type": "number" + }, + "baseTokenBalanceChange": { + "type": "number" + }, + "quoteTokenBalanceChange": { + "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" + } }, "required": [ "tokenIn", @@ -7007,7 +9117,11 @@ "type": "number", "example": 0.01 }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number", + "example": 2 + }, "positionAddress": { "description": "DAMM v2 positions are NFTs; a wallet may hold several per pool. Provide a position address (from position-info) to add to that specific position; omit to open a NEW position NFT.", "type": "string" @@ -7035,14 +9149,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -7107,14 +9232,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -7191,9 +9327,17 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was seeded at (quote per base)", "type": "number" @@ -7201,9 +9345,15 @@ "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -7222,14 +9372,23 @@ "description": "Fetch Orca pools from API with search and sorting", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "minimum": 1, "maximum": 100, "default": 50, "type": "number" }, + "schema": { + "minimum": 1, + "maximum": 100, + "default": 50, + "type": "number" + }, "example": 50, "in": "query", "name": "limit", @@ -7237,8 +9396,20 @@ "description": "Maximum number of pools to return" }, { - "schema": { "type": "string" }, - "examples": { "SOL": { "value": "SOL" }, "USDC": { "value": "USDC" }, "SOL-USDC": { "value": "SOL-USDC" } }, + "schema": { + "type": "string" + }, + "examples": { + "SOL": { + "value": "SOL" + }, + "USDC": { + "value": "USDC" + }, + "SOL-USDC": { + "value": "SOL-USDC" + } + }, "in": "query", "name": "query", "required": false, @@ -7256,14 +9427,21 @@ "description": "Sort by field" }, { - "schema": { "enum": ["asc", "desc"], "default": "desc", "type": "string" }, + "schema": { + "enum": ["asc", "desc"], + "default": "desc", + "type": "string" + }, "in": "query", "name": "sortDirection", "required": false, "description": "Sort direction" }, { - "schema": { "default": false, "type": "boolean" }, + "schema": { + "default": false, + "type": "boolean" + }, "in": "query", "name": "verifiedOnly", "required": false, @@ -7283,20 +9461,62 @@ "items": { "type": "object", "properties": { - "address": { "description": "Pool address", "type": "string" }, - "name": { "description": "Pool name (e.g., SOL-USDC)", "type": "string" }, - "baseTokenAddress": { "description": "Base token address", "type": "string" }, - "baseTokenSymbol": { "description": "Base token symbol", "type": "string" }, - "quoteTokenAddress": { "description": "Quote token address", "type": "string" }, - "quoteTokenSymbol": { "description": "Quote token symbol", "type": "string" }, - "binStep": { "description": "Bin step / tick spacing", "type": "number" }, - "baseFee": { "description": "Base fee percentage", "type": "number" }, - "price": { "description": "Current price", "type": "number" }, - "tvl": { "description": "Total value locked in USD", "type": "number" }, - "apr": { "description": "Annual percentage rate", "type": "number" }, - "apy": { "description": "Annual percentage yield", "type": "number" }, - "volume24h": { "description": "24-hour trading volume", "type": "number" }, - "fees24h": { "description": "24-hour fees collected", "type": "number" } + "address": { + "description": "Pool address", + "type": "string" + }, + "name": { + "description": "Pool name (e.g., SOL-USDC)", + "type": "string" + }, + "baseTokenAddress": { + "description": "Base token address", + "type": "string" + }, + "baseTokenSymbol": { + "description": "Base token symbol", + "type": "string" + }, + "quoteTokenAddress": { + "description": "Quote token address", + "type": "string" + }, + "quoteTokenSymbol": { + "description": "Quote token symbol", + "type": "string" + }, + "binStep": { + "description": "Bin step / tick spacing", + "type": "number" + }, + "baseFee": { + "description": "Base fee percentage", + "type": "number" + }, + "price": { + "description": "Current price", + "type": "number" + }, + "tvl": { + "description": "Total value locked in USD", + "type": "number" + }, + "apr": { + "description": "Annual percentage rate", + "type": "number" + }, + "apy": { + "description": "Annual percentage yield", + "type": "number" + }, + "volume24h": { + "description": "24-hour trading volume", + "type": "number" + }, + "fees24h": { + "description": "24-hour fees collected", + "type": "number" + } }, "required": [ "address", @@ -7312,9 +9532,18 @@ ] } }, - "total": { "description": "Total number of matching pools", "type": "number" }, - "page": { "description": "Current page number", "type": "number" }, - "pageSize": { "description": "Number of pools per page", "type": "number" } + "total": { + "description": "Total number of matching pools", + "type": "number" + }, + "page": { + "description": "Current page number", + "type": "number" + }, + "pageSize": { + "description": "Number of pools per page", + "type": "number" + } }, "required": ["pools", "total", "page", "pageSize"] } @@ -7330,14 +9559,20 @@ "description": "Get pool information for a Orca pool", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", "in": "query", "name": "poolAddress", @@ -7345,7 +9580,12 @@ "description": "Orca CLMM pool address" }, { - "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "schema": { + "default": 0, + "minimum": 0, + "maximum": 401, + "type": "integer" + }, "in": "query", "name": "binCount", "required": false, @@ -7360,33 +9600,69 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" }, + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "binStep": { + "type": "number" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "activeBinId": { + "type": "number" + }, "bins": { "type": "array", "items": { "type": "object", "properties": { - "binId": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } - }, - "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] - } + "binId": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] + } + }, + "liquidity": { + "type": "string" + }, + "sqrtPrice": { + "type": "string" + }, + "tvlUsdc": { + "type": "number" + }, + "protocolFeeRate": { + "type": "number" }, - "liquidity": { "type": "string" }, - "sqrtPrice": { "type": "string" }, - "tvlUsdc": { "type": "number" }, - "protocolFeeRate": { "type": "number" }, - "yieldOverTvl": { "type": "number" } + "yieldOverTvl": { + "type": "number" + } }, "required": [ "address", @@ -7416,14 +9692,21 @@ "description": "Retrieve all positions owned by a user's wallet across Orca CLMM pools", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", @@ -7441,19 +9724,45 @@ "items": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -7484,14 +9793,20 @@ "description": "Get details for a specific Orca position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "", "in": "query", "name": "positionAddress", @@ -7499,7 +9814,10 @@ "description": "Position address" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", @@ -7515,19 +9833,45 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -7557,14 +9901,20 @@ "description": "Quote amounts for a new Orca CLMM position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 200, "in": "query", "name": "lowerPrice", @@ -7572,7 +9922,9 @@ "description": "Lower price bound for the position" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 300, "in": "query", "name": "upperPrice", @@ -7580,7 +9932,9 @@ "description": "Upper price bound for the position" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", "in": "query", "name": "poolAddress", @@ -7588,7 +9942,9 @@ "description": "Orca CLMM pool address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -7596,7 +9952,9 @@ "description": "Amount of base token to deposit" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -7604,7 +9962,12 @@ "description": "Amount of quote token to deposit" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 1, + "type": "number" + }, "example": 1, "in": "query", "name": "slippagePct", @@ -7620,11 +9983,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" }, + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + }, "liquidity": {} }, "required": [ @@ -7647,14 +10020,20 @@ "description": "Get swap quote for Orca CLMM", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE", "in": "query", "name": "poolAddress", @@ -7662,7 +10041,9 @@ "description": "Orca CLMM pool address (optional - can be looked up from baseToken and quoteToken)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -7670,7 +10051,9 @@ "description": "Token to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -7678,7 +10061,9 @@ "description": "The other token in the pair (optional - required if poolAddress not provided)" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "amount", @@ -7686,7 +10071,11 @@ "description": "Amount to swap" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "example": "SELL", "in": "query", "name": "side", @@ -7694,7 +10083,12 @@ "description": "Trade direction" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 1, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 1, + "type": "number" + }, "example": 1, "in": "query", "name": "slippagePct", @@ -7710,16 +10104,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -7766,13 +10180,21 @@ "type": "string", "example": "Czfq3xZZDmsdGdUyrNLtRhGc47cXcZtLG4crryfu44zE" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, + "baseToken": { + "description": "Base token symbol or address", + "type": "string", + "example": "SOL" + }, "quoteToken": { "description": "Quote token symbol or address (optional - required if poolAddress not provided)", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, + "amount": { + "description": "Amount to swap", + "type": "number", + "example": 0.01 + }, "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], @@ -7803,18 +10225,41 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "fee": { + "type": "number" + }, + "baseTokenBalanceChange": { + "type": "number" + }, + "quoteTokenBalanceChange": { + "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" + } }, "required": [ "tokenIn", @@ -7893,14 +10338,30 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was initialized at (quote per base)", "type": "number" }, - "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + "data": { + "type": "object", + "properties": { + "fee": { + "type": "number" + } + }, + "required": ["fee"] + } }, "required": ["signature", "status", "poolAddress"] } @@ -7980,16 +10441,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "positionAddress": { + "type": "string" + }, + "positionRent": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": [ "fee", @@ -8068,14 +10544,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -8098,7 +10585,11 @@ "schema": { "type": "object", "properties": { - "network": { "description": "Solana network to use", "default": "mainnet-beta", "type": "string" }, + "network": { + "description": "Solana network to use", + "default": "mainnet-beta", + "type": "string" + }, "walletAddress": { "description": "Solana wallet address that will remove liquidity", "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", @@ -8141,14 +10632,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -8203,14 +10705,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -8265,24 +10778,41 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } - }, - "required": [ - "fee", - "positionRentRefunded", - "baseTokenAmountRemoved", - "quoteTokenAmountRemoved", - "baseFeeAmountCollected", + "fee": { + "type": "number" + }, + "positionRentRefunded": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } + }, + "required": [ + "fee", + "positionRentRefunded", + "baseTokenAmountRemoved", + "quoteTokenAmountRemoved", + "baseFeeAmountCollected", "quoteFeeAmountCollected" ] } @@ -8301,14 +10831,20 @@ "description": "Get AMM pool information from Raydium", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", "in": "query", "name": "poolAddress", @@ -8324,13 +10860,27 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": [ "address", @@ -8354,14 +10904,20 @@ "description": "Get info about a Raydium AMM position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", "in": "query", "name": "poolAddress", @@ -8369,7 +10925,10 @@ "description": "Raydium AMM pool address" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": false, @@ -8384,14 +10943,30 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "walletAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "lpTokenAmount": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "price": { "type": "number" }, + "poolAddress": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "lpTokenAmount": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "price": { + "type": "number" + }, "positions": { "type": "array", "items": { @@ -8405,8 +10980,12 @@ "description": "Liquidity held by this position (LP units)", "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] } @@ -8435,14 +11014,20 @@ "description": "Quote amounts for a new Raydium AMM liquidity position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", "in": "query", "name": "poolAddress", @@ -8450,7 +11035,9 @@ "description": "Raydium AMM pool address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -8458,7 +11045,9 @@ "description": "Amount of base token to add" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -8466,7 +11055,12 @@ "description": "Amount of quote token to add" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -8482,11 +11076,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" } + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + } }, "required": [ "baseLimited", @@ -8502,7 +11106,16 @@ "500": { "description": "Default Response", "content": { - "application/json": { "schema": { "type": "object", "properties": { "error": { "type": "string" } } } } + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + } + } } } } @@ -8514,14 +11127,20 @@ "description": "Get swap quote for Raydium AMM", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "58oQChx4yWmvKdwLLZzBi4ChoCc2fqCUWBkwMihLYQo2", "in": "query", "name": "poolAddress", @@ -8529,7 +11148,9 @@ "description": "AMM pool address (optional - can be looked up from baseToken and quoteToken)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -8537,7 +11158,9 @@ "description": "Token to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -8545,7 +11168,9 @@ "description": "The other token in the pair (optional - required if poolAddress not provided)" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "amount", @@ -8553,14 +11178,23 @@ "description": "Amount to swap" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -8576,16 +11210,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -8615,14 +11269,38 @@ "schema": { "type": "object", "properties": { - "walletAddress": { "type": "string", "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, - "network": { "type": "string", "default": "mainnet-beta" }, - "poolAddress": { "type": "string", "example": "" }, - "baseToken": { "type": "string", "example": "SOL" }, - "quoteToken": { "type": "string", "example": "USDC" }, - "amount": { "type": "number", "example": 0.01 }, - "side": { "type": "string", "example": "SELL" }, - "slippagePct": { "type": "number", "example": 1 } + "walletAddress": { + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "network": { + "type": "string", + "default": "mainnet-beta" + }, + "poolAddress": { + "type": "string", + "example": "" + }, + "baseToken": { + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "type": "string", + "example": "USDC" + }, + "amount": { + "type": "number", + "example": 0.01 + }, + "side": { + "type": "string", + "example": "SELL" + }, + "slippagePct": { + "type": "number", + "example": 1 + } }, "required": ["baseToken", "amount", "side"] } @@ -8638,18 +11316,41 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "fee": { + "type": "number" + }, + "baseTokenBalanceChange": { + "type": "number" + }, + "quoteTokenBalanceChange": { + "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" + } }, "required": [ "tokenIn", @@ -8701,7 +11402,11 @@ "type": "number", "example": 0.01 }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number", + "example": 2 + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -8725,14 +11430,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -8793,14 +11509,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -8885,9 +11612,17 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was seeded at (quote per base)", "type": "number" @@ -8895,9 +11630,15 @@ "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -8916,14 +11657,20 @@ "description": "Get CLMM pool information from Raydium", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", "in": "query", "name": "poolAddress", @@ -8931,7 +11678,12 @@ "description": "Raydium CLMM pool address" }, { - "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "schema": { + "default": 0, + "minimum": 0, + "maximum": 401, + "type": "integer" + }, "in": "query", "name": "binCount", "required": false, @@ -8946,29 +11698,55 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" }, - "bins": { - "type": "array", - "items": { - "type": "object", - "properties": { - "binId": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } - }, - "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] - } - } - }, + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "binStep": { + "type": "number" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "activeBinId": { + "type": "number" + }, + "bins": { + "type": "array", + "items": { + "type": "object", + "properties": { + "binId": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } + }, + "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] + } + } + }, "required": [ "address", "baseTokenAddress", @@ -8992,14 +11770,20 @@ "description": "Retrieve all positions owned by a user's wallet across all Raydium CLMM pools", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", @@ -9017,19 +11801,45 @@ "items": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -9060,14 +11870,20 @@ "description": "Get info about a Raydium CLMM position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "", "in": "query", "name": "positionAddress", @@ -9075,7 +11891,10 @@ "description": "Position NFT address" }, { - "schema": { "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, + "schema": { + "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": false, @@ -9090,19 +11909,45 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -9132,14 +11977,20 @@ "description": "Quote amounts for a new Raydium CLMM position", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 100, "in": "query", "name": "lowerPrice", @@ -9147,7 +11998,9 @@ "description": "Lower price bound for the position" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 300, "in": "query", "name": "upperPrice", @@ -9155,7 +12008,9 @@ "description": "Upper price bound for the position" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", "in": "query", "name": "poolAddress", @@ -9163,7 +12018,9 @@ "description": "Raydium CLMM pool address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -9171,7 +12028,9 @@ "description": "Amount of base token to deposit" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -9179,7 +12038,12 @@ "description": "Amount of quote token to deposit" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -9195,11 +12059,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" }, + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + }, "liquidity": {} }, "required": [ @@ -9222,14 +12096,20 @@ "description": "Get swap quote for Raydium CLMM", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv", "in": "query", "name": "poolAddress", @@ -9237,7 +12117,9 @@ "description": "CLMM pool address (optional - can be looked up from tokens)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -9245,7 +12127,9 @@ "description": "Token to determine swap direction" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -9253,7 +12137,9 @@ "description": "The other token in the pair" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "amount", @@ -9261,14 +12147,23 @@ "description": "Amount to swap" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -9284,16 +12179,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -9340,9 +12255,21 @@ "type": "string", "example": "3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, - "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, + "baseToken": { + "description": "Base token symbol or address", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "Quote token symbol or address", + "type": "string", + "example": "USDC" + }, + "amount": { + "description": "Amount to swap", + "type": "number", + "example": 0.01 + }, "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], @@ -9373,18 +12300,41 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "fee": { + "type": "number" + }, + "baseTokenBalanceChange": { + "type": "number" + }, + "quoteTokenBalanceChange": { + "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" + } }, "required": [ "tokenIn", @@ -9474,16 +12424,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "positionAddress": { + "type": "string" + }, + "positionRent": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": [ "fee", @@ -9559,14 +12524,30 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was initialized at (quote per base)", "type": "number" }, - "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + "data": { + "type": "object", + "properties": { + "fee": { + "type": "number" + } + }, + "required": ["fee"] + } }, "required": ["signature", "status", "poolAddress"] } @@ -9607,7 +12588,11 @@ "type": "number", "example": 0.01 }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number", + "example": 2 + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -9631,14 +12616,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -9699,14 +12695,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -9729,12 +12736,20 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "mainnet-beta" }, - "walletAddress": { "type": "string", "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" }, - "positionAddress": { "type": "string" } - }, - "required": ["positionAddress"] - } + "network": { + "type": "string", + "default": "mainnet-beta" + }, + "walletAddress": { + "type": "string", + "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5" + }, + "positionAddress": { + "type": "string" + } + }, + "required": ["positionAddress"] + } } }, "required": true @@ -9747,14 +12762,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -9808,17 +12834,34 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "positionRentRefunded": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": [ "fee", @@ -9866,7 +12909,9 @@ "description": "The EVM network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "WETH", "in": "query", "name": "baseToken", @@ -9874,7 +12919,9 @@ "description": "First token in the trading pair" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -9882,7 +12929,9 @@ "description": "Second token in the trading pair" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.001, "in": "query", "name": "amount", @@ -9890,21 +12939,32 @@ "description": "Amount of base token to trade" }, { - "schema": { "enum": ["BUY", "SELL"], "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, + "schema": { + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": false, @@ -9919,19 +12979,46 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "quoteId": { + "description": "Unique identifier for this quote", + "type": "string" + }, + "tokenIn": { + "description": "Address of the token being swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token being swapped to", + "type": "string" + }, + "amountIn": { + "description": "Amount of tokenIn to be swapped", + "type": "number" + }, + "amountOut": { + "description": "Expected amount of tokenOut to receive", + "type": "number" + }, + "price": { + "description": "Exchange rate between tokenIn and tokenOut", + "type": "number" + }, + "priceImpactPct": { + "description": "Estimated price impact percentage (0-100)", + "type": "number" + }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, - "routePath": { "description": "Human-readable route path", "type": "string" } + "maxAmountIn": { + "description": "Maximum amount of tokenIn that will be spent", + "type": "number" + }, + "routePath": { + "description": "Human-readable route path", + "type": "string" + } }, "required": [ "quoteId", @@ -10004,7 +13091,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -10012,11 +13102,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -10024,6 +13129,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -10083,8 +13192,16 @@ "type": "string", "example": "WETH" }, - "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.001 }, + "quoteToken": { + "description": "The other token in the pair", + "type": "string", + "example": "USDC" + }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 0.001 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -10113,7 +13230,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -10121,11 +13241,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -10133,6 +13268,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -10182,7 +13321,9 @@ "description": "The EVM network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "0x88A43bbDF9D098eEC7bCEda4e2494615dfD9bB9C", "in": "query", "name": "poolAddress", @@ -10198,13 +13339,27 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": [ "address", @@ -10227,17 +13382,51 @@ "tags": ["/connector/uniswap"], "description": "Get position information for a Uniswap V2 pool", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { - "schema": { "type": "string" }, + "schema": { + "type": "string", + "default": "base" + }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { + "type": "string" + }, "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "in": "query", "name": "walletAddress", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } + { + "schema": { + "type": "string" + }, + "example": "", + "in": "query", + "name": "poolAddress", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "WETH", + "in": "query", + "name": "baseToken", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false + } ], "responses": { "200": { @@ -10247,14 +13436,30 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "walletAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "lpTokenAmount": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "price": { "type": "number" }, + "poolAddress": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "lpTokenAmount": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "price": { + "type": "number" + }, "positions": { "type": "array", "items": { @@ -10268,8 +13473,12 @@ "description": "Liquidity held by this position (LP units)", "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] } @@ -10297,19 +13506,70 @@ "tags": ["/connector/uniswap"], "description": "Get swap quote for Uniswap V2 AMM", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": false }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, - { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, { - "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "schema": { + "type": "string", + "default": "base" + }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "", + "in": "query", + "name": "poolAddress", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "WETH", + "in": "query", + "name": "baseToken", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false + }, + { + "schema": { + "type": "number" + }, + "example": 0.001, + "in": "query", + "name": "amount", + "required": true + }, + { + "schema": { + "type": "string", + "enum": ["BUY", "SELL"] + }, "example": "SELL", "in": "query", "name": "side", "required": true }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } + { + "schema": { + "type": "number" + }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false + } ], "responses": { "200": { @@ -10319,31 +13579,51 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } - }, - "required": [ - "poolAddress", - "tokenIn", - "tokenOut", - "amountIn", - "amountOut", - "price", - "minAmountOut", - "maxAmountIn", - "priceImpactPct" - ] - } - } - } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } + }, + "required": [ + "poolAddress", + "tokenIn", + "tokenOut", + "amountIn", + "amountOut", + "price", + "minAmountOut", + "maxAmountIn", + "priceImpactPct" + ] + } + } + } } } } @@ -10353,25 +13633,69 @@ "tags": ["/connector/uniswap"], "description": "Get liquidity quote for a Uniswap V2 pool", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, { - "schema": { "type": "number" }, + "schema": { + "type": "string", + "default": "base" + }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "", + "in": "query", + "name": "poolAddress", + "required": true + }, + { + "schema": { + "type": "number" + }, "example": 0.001, "in": "query", "name": "baseTokenAmount", "required": true }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2.5, "in": "query", "name": "quoteTokenAmount", "required": true }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } + { + "schema": { + "type": "number" + }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "WETH", + "in": "query", + "name": "baseToken", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false + } ], "responses": { "200": { @@ -10381,11 +13705,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" } + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + } }, "required": [ "baseLimited", @@ -10438,10 +13772,26 @@ "default": "", "type": "string" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "WETH" }, - "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 0.001 }, - "side": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "baseToken": { + "description": "Base token symbol or address", + "type": "string", + "example": "WETH" + }, + "quoteToken": { + "description": "Quote token symbol or address", + "type": "string", + "example": "USDC" + }, + "amount": { + "description": "Amount to swap", + "type": "number", + "example": 0.001 + }, + "side": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -10464,7 +13814,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -10472,11 +13825,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -10484,6 +13852,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -10537,9 +13909,18 @@ "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "poolAddress": { "description": "Address of the Uniswap V2 pool", "type": "string" }, - "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "poolAddress": { + "description": "Address of the Uniswap V2 pool", + "type": "string" + }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number" + }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number" + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -10562,14 +13943,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -10624,7 +14016,10 @@ "type": "string", "example": "USDC" }, - "baseTokenAmount": { "description": "Amount of base token to seed the pool with", "type": "number" }, + "baseTokenAmount": { + "description": "Amount of base token to seed the pool with", + "type": "number" + }, "quoteTokenAmount": { "description": "Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. If omitted (and no initialPrice), the current market price is fetched from the unified swap router.", "type": "number" @@ -10655,9 +14050,17 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was seeded at (quote per base)", "type": "number" @@ -10665,9 +14068,15 @@ "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -10712,7 +14121,10 @@ "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "poolAddress": { "description": "Address of the Uniswap V2 pool", "type": "string" }, + "poolAddress": { + "description": "Address of the Uniswap V2 pool", + "type": "string" + }, "percentageToRemove": { "minimum": 0, "maximum": 100, @@ -10734,14 +14146,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -10782,7 +14205,9 @@ "description": "The EVM network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "0xd0b53d9277642d899df5c87a3966a349a798f224", "in": "query", "name": "poolAddress", @@ -10790,7 +14215,12 @@ "description": "Uniswap V3 pool address" }, { - "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "schema": { + "default": 0, + "minimum": 0, + "maximum": 401, + "type": "integer" + }, "in": "query", "name": "binCount", "required": false, @@ -10805,24 +14235,50 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" }, + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "binStep": { + "type": "number" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "activeBinId": { + "type": "number" + }, "bins": { "type": "array", "items": { "type": "object", "properties": { - "binId": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "binId": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] } @@ -10850,9 +14306,19 @@ "tags": ["/connector/uniswap"], "description": "Get position information for a Uniswap V3 position", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { - "schema": { "type": "string" }, + "schema": { + "type": "string", + "default": "base" + }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { + "type": "string" + }, "example": "1234", "in": "query", "name": "positionAddress", @@ -10868,19 +14334,45 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -10910,14 +14402,19 @@ "description": "Get all Uniswap V3 positions owned by a wallet", "parameters": [ { - "schema": { "default": "base", "type": "string" }, + "schema": { + "default": "base", + "type": "string" + }, "example": "base", "in": "query", "name": "network", "required": false }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "in": "query", "name": "walletAddress", @@ -10934,22 +14431,48 @@ "items": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } - }, - "required": [ - "address", + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } + }, + "required": [ + "address", "poolAddress", "baseTokenAddress", "quoteTokenAddress", @@ -10977,37 +14500,67 @@ "description": "Get a quote for opening a position on Uniswap V3", "parameters": [ { - "schema": { "type": "string", "default": "base" }, + "schema": { + "type": "string", + "default": "base" + }, "example": "base", "in": "query", "name": "network", "required": false }, - { "schema": { "type": "number" }, "example": 2000, "in": "query", "name": "lowerPrice", "required": true }, - { "schema": { "type": "number" }, "example": 4000, "in": "query", "name": "upperPrice", "required": true }, { - "schema": { "type": "string", "default": "0xd0b53d9277642d899df5c87a3966a349a798f224" }, + "schema": { + "type": "number" + }, + "example": 2000, + "in": "query", + "name": "lowerPrice", + "required": true + }, + { + "schema": { + "type": "number" + }, + "example": 4000, + "in": "query", + "name": "upperPrice", + "required": true + }, + { + "schema": { + "type": "string", + "default": "0xd0b53d9277642d899df5c87a3966a349a798f224" + }, "example": "0xd0b53d9277642d899df5c87a3966a349a798f224", "in": "query", "name": "poolAddress", "required": true }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.001, "in": "query", "name": "baseTokenAmount", "required": false }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 3, "in": "query", "name": "quoteTokenAmount", "required": false }, { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "type": "number" + }, "in": "query", "name": "slippagePct", "required": false @@ -11021,11 +14574,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" }, + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + }, "liquidity": {} }, "required": [ @@ -11047,25 +14610,70 @@ "tags": ["/connector/uniswap"], "description": "Get swap quote for Uniswap V3 CLMM", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { - "schema": { "type": "string" }, + "schema": { + "type": "string", + "default": "base" + }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { + "type": "string" + }, "in": "query", "name": "poolAddress", "required": false, "description": "Pool address (optional - can be looked up from baseToken and quoteToken)" }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, - { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, { - "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "schema": { + "type": "string" + }, + "example": "WETH", + "in": "query", + "name": "baseToken", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false + }, + { + "schema": { + "type": "number" + }, + "example": 0.001, + "in": "query", + "name": "amount", + "required": true + }, + { + "schema": { + "type": "string", + "enum": ["BUY", "SELL"] + }, "example": "SELL", "in": "query", "name": "side", "required": true }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } + { + "schema": { + "type": "number" + }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false + } ], "responses": { "200": { @@ -11075,16 +14683,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -11142,8 +14770,16 @@ "type": "string", "example": "WETH" }, - "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 0.001 }, + "quoteToken": { + "description": "The other token in the pair", + "type": "string", + "example": "USDC" + }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 0.001 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -11172,7 +14808,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -11180,11 +14819,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -11192,6 +14846,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -11223,14 +14881,38 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "base" }, - "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, - "lowerPrice": { "type": "number", "example": 1000 }, - "upperPrice": { "type": "number", "example": 4000 }, - "poolAddress": { "type": "string", "example": "0xd0b53d9277642d899df5c87a3966a349a798f224" }, - "baseTokenAmount": { "type": "number", "example": 0.001 }, - "quoteTokenAmount": { "type": "number", "example": 3 }, - "slippagePct": { "type": "number", "example": 1 } + "network": { + "type": "string", + "default": "base" + }, + "walletAddress": { + "type": "string", + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" + }, + "lowerPrice": { + "type": "number", + "example": 1000 + }, + "upperPrice": { + "type": "number", + "example": 4000 + }, + "poolAddress": { + "type": "string", + "example": "0xd0b53d9277642d899df5c87a3966a349a798f224" + }, + "baseTokenAmount": { + "type": "number", + "example": 0.001 + }, + "quoteTokenAmount": { + "type": "number", + "example": 3 + }, + "slippagePct": { + "type": "number", + "example": 1 + } }, "required": ["lowerPrice", "upperPrice", "poolAddress"] } @@ -11246,16 +14928,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "positionAddress": { + "type": "string" + }, + "positionRent": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": [ "fee", @@ -11341,14 +15038,30 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was initialized at (quote per base)", "type": "number" }, - "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + "data": { + "type": "object", + "properties": { + "fee": { + "type": "number" + } + }, + "required": ["fee"] + } }, "required": ["signature", "status", "poolAddress"] } @@ -11390,9 +15103,18 @@ "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "positionAddress": { "description": "NFT token ID of the position", "type": "string" }, - "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "positionAddress": { + "description": "NFT token ID of the position", + "type": "string" + }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number" + }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number" + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -11415,14 +15137,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -11445,10 +15178,25 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "base" }, - "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, - "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" }, - "percentageToRemove": { "type": "number", "minimum": 0, "maximum": 100, "example": 50 } + "network": { + "type": "string", + "default": "base" + }, + "walletAddress": { + "type": "string", + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" + }, + "positionAddress": { + "type": "string", + "description": "Position NFT token ID", + "example": "1234" + }, + "percentageToRemove": { + "type": "number", + "minimum": 0, + "maximum": 100, + "example": 50 + } }, "required": ["positionAddress", "percentageToRemove"] } @@ -11464,14 +15212,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -11494,9 +15253,19 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "base" }, - "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, - "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" } + "network": { + "type": "string", + "default": "base" + }, + "walletAddress": { + "type": "string", + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" + }, + "positionAddress": { + "type": "string", + "description": "Position NFT token ID", + "example": "1234" + } }, "required": ["positionAddress"] } @@ -11512,14 +15281,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "data": { - "type": "object", - "properties": { - "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "data": { + "type": "object", + "properties": { + "fee": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -11542,9 +15322,15 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string" }, - "walletAddress": { "type": "string" }, - "positionAddress": { "type": "string" } + "network": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "positionAddress": { + "type": "string" + } }, "required": ["positionAddress"] } @@ -11560,17 +15346,34 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "positionRentRefunded": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": [ "fee", @@ -11607,7 +15410,9 @@ "description": "The EVM network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "WETH", "in": "query", "name": "baseToken", @@ -11615,7 +15420,9 @@ "description": "First token in the trading pair" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -11623,7 +15430,9 @@ "description": "Second token in the trading pair" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 1, "in": "query", "name": "amount", @@ -11631,14 +15440,21 @@ "description": "Amount of base token to trade" }, { - "schema": { "enum": ["BUY", "SELL"], "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "type": "number" + }, "example": 1, "in": "query", "name": "slippagePct", @@ -11646,14 +15462,19 @@ "description": "Maximum acceptable slippage percentage" }, { - "schema": { "default": true, "type": "boolean" }, + "schema": { + "default": true, + "type": "boolean" + }, "in": "query", "name": "indicativePrice", "required": false, "description": "If true, returns indicative pricing only (no commitment). If false, returns firm quote ready for execution" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "query", "name": "takerAddress", "required": false, @@ -11668,31 +15489,71 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "quoteId": { + "description": "Unique identifier for this quote", + "type": "string" + }, + "tokenIn": { + "description": "Address of the token being swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token being swapped to", + "type": "string" + }, + "amountIn": { + "description": "Amount of tokenIn to be swapped", + "type": "number" + }, + "amountOut": { + "description": "Expected amount of tokenOut to receive", + "type": "number" + }, + "price": { + "description": "Exchange rate between tokenIn and tokenOut", + "type": "number" + }, + "priceImpactPct": { + "description": "Estimated price impact percentage (0-100)", + "type": "number" + }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, + "maxAmountIn": { + "description": "Maximum amount of tokenIn that will be spent", + "type": "number" + }, "expirationTime": { "description": "Unix timestamp when this quote expires (only for firm quotes)", "type": "number" }, - "gasEstimate": { "description": "Estimated gas required for the swap", "type": "string" }, - "sources": { "description": "Liquidity sources used for this quote", "type": "array", "items": {} }, + "gasEstimate": { + "description": "Estimated gas required for the swap", + "type": "string" + }, + "sources": { + "description": "Liquidity sources used for this quote", + "type": "array", + "items": {} + }, "allowanceTarget": { "description": "Contract address that needs token approval", "type": "string" }, - "to": { "description": "Contract address to send transaction to", "type": "string" }, - "data": { "description": "Encoded transaction data", "type": "string" }, - "value": { "description": "ETH value to send with transaction", "type": "string" } + "to": { + "description": "Contract address to send transaction to", + "type": "string" + }, + "data": { + "description": "Encoded transaction data", + "type": "string" + }, + "value": { + "description": "ETH value to send with transaction", + "type": "string" + } }, "required": [ "quoteId", @@ -11755,7 +15616,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -11763,11 +15627,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -11775,6 +15654,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -11824,8 +15707,16 @@ "type": "string", "example": "WETH" }, - "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 1 }, + "quoteToken": { + "description": "The other token in the pair", + "type": "string", + "example": "USDC" + }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 1 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -11853,7 +15744,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -11861,11 +15755,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -11873,6 +15782,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -11900,14 +15813,20 @@ "description": "Get an executable swap quote from Pancakeswap Universal Router", "parameters": [ { - "schema": { "default": "mainnet", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, + "schema": { + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "The EVM network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDT", "in": "query", "name": "baseToken", @@ -11915,7 +15834,9 @@ "description": "First token in the trading pair" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "WBNB", "in": "query", "name": "quoteToken", @@ -11923,7 +15844,9 @@ "description": "Second token in the trading pair" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 10, "in": "query", "name": "amount", @@ -11931,21 +15854,32 @@ "description": "Amount of base token to trade" }, { - "schema": { "enum": ["BUY", "SELL"], "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "in": "query", "name": "slippagePct", "required": false, "description": "Maximum acceptable slippage percentage" }, { - "schema": { "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, + "schema": { + "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", + "type": "string" + }, "in": "query", "name": "walletAddress", "required": false, @@ -11960,19 +15894,46 @@ "schema": { "type": "object", "properties": { - "quoteId": { "description": "Unique identifier for this quote", "type": "string" }, - "tokenIn": { "description": "Address of the token being swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token being swapped to", "type": "string" }, - "amountIn": { "description": "Amount of tokenIn to be swapped", "type": "number" }, - "amountOut": { "description": "Expected amount of tokenOut to receive", "type": "number" }, - "price": { "description": "Exchange rate between tokenIn and tokenOut", "type": "number" }, - "priceImpactPct": { "description": "Estimated price impact percentage (0-100)", "type": "number" }, + "quoteId": { + "description": "Unique identifier for this quote", + "type": "string" + }, + "tokenIn": { + "description": "Address of the token being swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token being swapped to", + "type": "string" + }, + "amountIn": { + "description": "Amount of tokenIn to be swapped", + "type": "number" + }, + "amountOut": { + "description": "Expected amount of tokenOut to receive", + "type": "number" + }, + "price": { + "description": "Exchange rate between tokenIn and tokenOut", + "type": "number" + }, + "priceImpactPct": { + "description": "Estimated price impact percentage (0-100)", + "type": "number" + }, "minAmountOut": { "description": "Minimum amount of tokenOut that will be accepted", "type": "number" }, - "maxAmountIn": { "description": "Maximum amount of tokenIn that will be spent", "type": "number" }, - "routePath": { "description": "Human-readable route path", "type": "string" } + "maxAmountIn": { + "description": "Maximum amount of tokenIn that will be spent", + "type": "number" + }, + "routePath": { + "description": "Human-readable route path", + "type": "string" + } }, "required": [ "quoteId", @@ -12034,7 +15995,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -12042,11 +16006,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -12054,6 +16033,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -12102,8 +16085,16 @@ "type": "string", "example": "USDT" }, - "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "WBNB" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 10 }, + "quoteToken": { + "description": "The other token in the pair", + "type": "string", + "example": "WBNB" + }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 10 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -12132,7 +16123,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -12140,11 +16134,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -12152,6 +16161,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -12179,14 +16192,20 @@ "description": "Get AMM pool information from Pancakeswap V2", "parameters": [ { - "schema": { "default": "mainnet", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, + "schema": { + "default": "mainnet", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "The EVM network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "0x88A43bbDF9D098eEC7bCEda4e2494615dfD9bB9C", "in": "query", "name": "poolAddress", @@ -12202,13 +16221,27 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": [ "address", @@ -12231,17 +16264,51 @@ "tags": ["/connector/pancakeswap"], "description": "Get position information for a Pancakeswap V2 pool", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, { - "schema": { "type": "string" }, + "schema": { + "type": "string", + "default": "base" + }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { + "type": "string" + }, "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "in": "query", "name": "walletAddress", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } + { + "schema": { + "type": "string" + }, + "example": "", + "in": "query", + "name": "poolAddress", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "WETH", + "in": "query", + "name": "baseToken", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false + } ], "responses": { "200": { @@ -12251,14 +16318,30 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "walletAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "lpTokenAmount": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "price": { "type": "number" }, + "poolAddress": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "lpTokenAmount": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "price": { + "type": "number" + }, "positions": { "type": "array", "items": { @@ -12272,8 +16355,12 @@ "description": "Liquidity held by this position (LP units)", "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["positionAddress", "lpTokenAmount", "baseTokenAmount", "quoteTokenAmount"] } @@ -12301,19 +16388,70 @@ "tags": ["/connector/pancakeswap"], "description": "Get swap quote for Pancakeswap V2 AMM", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": false }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": true }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false }, - { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "amount", "required": true }, { - "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "schema": { + "type": "string", + "default": "base" + }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "", + "in": "query", + "name": "poolAddress", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "WETH", + "in": "query", + "name": "baseToken", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false + }, + { + "schema": { + "type": "number" + }, + "example": 0.001, + "in": "query", + "name": "amount", + "required": true + }, + { + "schema": { + "type": "string", + "enum": ["BUY", "SELL"] + }, "example": "SELL", "in": "query", "name": "side", "required": true }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } + { + "schema": { + "type": "number" + }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false + } ], "responses": { "200": { @@ -12323,16 +16461,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -12357,25 +16515,69 @@ "tags": ["/connector/pancakeswap"], "description": "Get liquidity quote for a Pancakeswap V2 pool", "parameters": [ - { "schema": { "type": "string", "default": "base" }, "in": "query", "name": "network", "required": false }, - { "schema": { "type": "string" }, "example": "", "in": "query", "name": "poolAddress", "required": true }, { - "schema": { "type": "number" }, + "schema": { + "type": "string", + "default": "base" + }, + "in": "query", + "name": "network", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "", + "in": "query", + "name": "poolAddress", + "required": true + }, + { + "schema": { + "type": "number" + }, "example": 0.001, "in": "query", "name": "baseTokenAmount", "required": true }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2.5, "in": "query", "name": "quoteTokenAmount", "required": true }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false }, - { "schema": { "type": "string" }, "example": "WETH", "in": "query", "name": "baseToken", "required": false }, - { "schema": { "type": "string" }, "example": "USDC", "in": "query", "name": "quoteToken", "required": false } + { + "schema": { + "type": "number" + }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "WETH", + "in": "query", + "name": "baseToken", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "USDC", + "in": "query", + "name": "quoteToken", + "required": false + } ], "responses": { "200": { @@ -12385,11 +16587,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" } + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + } }, "required": [ "baseLimited", @@ -12431,10 +16643,26 @@ "default": "", "type": "string" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "USDT" }, - "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "WBNB" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 10 }, - "side": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "baseToken": { + "description": "Base token symbol or address", + "type": "string", + "example": "USDT" + }, + "quoteToken": { + "description": "Quote token symbol or address", + "type": "string", + "example": "WBNB" + }, + "amount": { + "description": "Amount to swap", + "type": "number", + "example": 10 + }, + "side": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -12457,7 +16685,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -12465,11 +16696,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -12477,6 +16723,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -12519,9 +16769,18 @@ "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "poolAddress": { "description": "Address of the Pancakeswap V2 pool", "type": "string" }, - "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "poolAddress": { + "description": "Address of the Pancakeswap V2 pool", + "type": "string" + }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number" + }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number" + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -12544,14 +16803,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -12585,7 +16855,10 @@ "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "poolAddress": { "description": "Address of the Pancakeswap V2 pool", "type": "string" }, + "poolAddress": { + "description": "Address of the Pancakeswap V2 pool", + "type": "string" + }, "percentageToRemove": { "minimum": 0, "maximum": 100, @@ -12607,14 +16880,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -12658,7 +16942,10 @@ "type": "string", "example": "WBNB" }, - "baseTokenAmount": { "description": "Amount of base token to seed the pool with", "type": "number" }, + "baseTokenAmount": { + "description": "Amount of base token to seed the pool with", + "type": "number" + }, "quoteTokenAmount": { "description": "Amount of quote token to seed with. If provided, the base:quote ratio sets the initial price. If omitted (and no initialPrice), the current market price is fetched from the unified swap router.", "type": "number" @@ -12689,9 +16976,17 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was seeded at (quote per base)", "type": "number" @@ -12699,9 +16994,15 @@ "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -12720,7 +17021,11 @@ "description": "Get CLMM pool information from Pancakeswap V3", "parameters": [ { - "schema": { "default": "bsc", "enum": ["arbitrum", "base", "bsc", "mainnet"], "type": "string" }, + "schema": { + "default": "bsc", + "enum": ["arbitrum", "base", "bsc", "mainnet"], + "type": "string" + }, "example": "bsc", "in": "query", "name": "network", @@ -12728,7 +17033,9 @@ "description": "The EVM network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "0x172fcd41e0913e95784454622d1c3724f546f849", "in": "query", "name": "poolAddress", @@ -12736,7 +17043,12 @@ "description": "Pancakeswap V3 pool address" }, { - "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "schema": { + "default": 0, + "minimum": 0, + "maximum": 401, + "type": "integer" + }, "in": "query", "name": "binCount", "required": false, @@ -12751,24 +17063,50 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" }, + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "binStep": { + "type": "number" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "activeBinId": { + "type": "number" + }, "bins": { "type": "array", "items": { "type": "object", "properties": { - "binId": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "binId": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] } @@ -12797,14 +17135,19 @@ "description": "Get position information for a Pancakeswap V3 position", "parameters": [ { - "schema": { "type": "string", "default": "bsc" }, + "schema": { + "type": "string", + "default": "bsc" + }, "example": "bsc", "in": "query", "name": "network", "required": false }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "1234", "in": "query", "name": "positionAddress", @@ -12820,19 +17163,45 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -12862,14 +17231,19 @@ "description": "Get all Pancakeswap V3 positions owned by a wallet", "parameters": [ { - "schema": { "default": "bsc", "type": "string" }, + "schema": { + "default": "bsc", + "type": "string" + }, "example": "bsc", "in": "query", "name": "network", "required": false }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "in": "query", "name": "walletAddress", @@ -12886,19 +17260,45 @@ "items": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -12929,37 +17329,67 @@ "description": "Get a quote for opening a position on Pancakeswap V3", "parameters": [ { - "schema": { "type": "string", "default": "bsc" }, + "schema": { + "type": "string", + "default": "bsc" + }, "example": "bsc", "in": "query", "name": "network", "required": false }, - { "schema": { "type": "number" }, "example": 0.0008, "in": "query", "name": "lowerPrice", "required": true }, - { "schema": { "type": "number" }, "example": 0.001, "in": "query", "name": "upperPrice", "required": true }, { - "schema": { "type": "string", "default": "0x172fcd41e0913e95784454622d1c3724f546f849" }, + "schema": { + "type": "number" + }, + "example": 0.0008, + "in": "query", + "name": "lowerPrice", + "required": true + }, + { + "schema": { + "type": "number" + }, + "example": 0.001, + "in": "query", + "name": "upperPrice", + "required": true + }, + { + "schema": { + "type": "string", + "default": "0x172fcd41e0913e95784454622d1c3724f546f849" + }, "example": "0x172fcd41e0913e95784454622d1c3724f546f849", "in": "query", "name": "poolAddress", "required": true }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 10, "in": "query", "name": "baseTokenAmount", "required": false }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "quoteTokenAmount", "required": false }, { - "schema": { "minimum": 0, "maximum": 100, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "type": "number" + }, "in": "query", "name": "slippagePct", "required": false @@ -12973,11 +17403,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" }, + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + }, "liquidity": {} }, "required": [ @@ -13000,30 +17440,70 @@ "description": "Get swap quote for Pancakeswap V3 CLMM", "parameters": [ { - "schema": { "type": "string", "default": "bsc" }, + "schema": { + "type": "string", + "default": "bsc" + }, "example": "bsc", "in": "query", "name": "network", "required": false }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "in": "query", "name": "poolAddress", "required": false, "description": "Pool address (optional - can be looked up from baseToken and quoteToken)" }, - { "schema": { "type": "string" }, "example": "USDT", "in": "query", "name": "baseToken", "required": true }, - { "schema": { "type": "string" }, "example": "WBNB", "in": "query", "name": "quoteToken", "required": false }, - { "schema": { "type": "number" }, "example": 10, "in": "query", "name": "amount", "required": true }, { - "schema": { "type": "string", "enum": ["BUY", "SELL"] }, + "schema": { + "type": "string" + }, + "example": "USDT", + "in": "query", + "name": "baseToken", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "WBNB", + "in": "query", + "name": "quoteToken", + "required": false + }, + { + "schema": { + "type": "number" + }, + "example": 10, + "in": "query", + "name": "amount", + "required": true + }, + { + "schema": { + "type": "string", + "enum": ["BUY", "SELL"] + }, "example": "SELL", "in": "query", "name": "side", "required": true }, - { "schema": { "type": "number" }, "example": 1, "in": "query", "name": "slippagePct", "required": false } + { + "schema": { + "type": "number" + }, + "example": 1, + "in": "query", + "name": "slippagePct", + "required": false + } ], "responses": { "200": { @@ -13033,16 +17513,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -13089,8 +17589,16 @@ "type": "string", "example": "USDT" }, - "quoteToken": { "description": "The other token in the pair", "type": "string", "example": "WBNB" }, - "amount": { "description": "Amount of base token to trade", "type": "number", "example": 10 }, + "quoteToken": { + "description": "The other token in the pair", + "type": "string", + "example": "WBNB" + }, + "amount": { + "description": "Amount of base token to trade", + "type": "number", + "example": 10 + }, "side": { "description": "Trade direction - BUY means buying base token with quote token, SELL means selling base token for quote token", "enum": ["BUY", "SELL"], @@ -13119,7 +17627,10 @@ "schema": { "type": "object", "properties": { - "signature": { "description": "Transaction signature/hash", "type": "string" }, + "signature": { + "description": "Transaction signature/hash", + "type": "string" + }, "status": { "description": "Transaction status: 0 = PENDING, 1 = CONFIRMED, -1 = FAILED", "type": "number" @@ -13127,11 +17638,26 @@ "data": { "type": "object", "properties": { - "tokenIn": { "description": "Address of the token swapped from", "type": "string" }, - "tokenOut": { "description": "Address of the token swapped to", "type": "string" }, - "amountIn": { "description": "Actual amount of tokenIn swapped", "type": "number" }, - "amountOut": { "description": "Actual amount of tokenOut received", "type": "number" }, - "fee": { "description": "Transaction fee paid", "type": "number" }, + "tokenIn": { + "description": "Address of the token swapped from", + "type": "string" + }, + "tokenOut": { + "description": "Address of the token swapped to", + "type": "string" + }, + "amountIn": { + "description": "Actual amount of tokenIn swapped", + "type": "number" + }, + "amountOut": { + "description": "Actual amount of tokenOut received", + "type": "number" + }, + "fee": { + "description": "Transaction fee paid", + "type": "number" + }, "baseTokenBalanceChange": { "description": "Change in base token balance (negative for decrease)", "type": "number" @@ -13139,6 +17665,10 @@ "quoteTokenBalanceChange": { "description": "Change in quote token balance (negative for decrease)", "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" } }, "required": [ @@ -13170,18 +17700,40 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "bsc", "example": "bsc" }, - "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, - "lowerPrice": { "type": "number", "example": 0.0008 }, - "upperPrice": { "type": "number", "example": 0.001 }, + "network": { + "type": "string", + "default": "bsc", + "example": "bsc" + }, + "walletAddress": { + "type": "string", + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" + }, + "lowerPrice": { + "type": "number", + "example": 0.0008 + }, + "upperPrice": { + "type": "number", + "example": 0.001 + }, "poolAddress": { "type": "string", "default": "0x172fcd41e0913e95784454622d1c3724f546f849", "example": "0x172fcd41e0913e95784454622d1c3724f546f849" }, - "baseTokenAmount": { "type": "number", "example": 10 }, - "quoteTokenAmount": { "type": "number", "example": 0.01 }, - "slippagePct": { "type": "number", "example": 1 } + "baseTokenAmount": { + "type": "number", + "example": 10 + }, + "quoteTokenAmount": { + "type": "number", + "example": 0.01 + }, + "slippagePct": { + "type": "number", + "example": 1 + } }, "required": ["lowerPrice", "upperPrice", "poolAddress"] } @@ -13197,16 +17749,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "positionAddress": { + "type": "string" + }, + "positionRent": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": [ "fee", @@ -13247,9 +17814,18 @@ "default": "0xDA50C69342216b538Daf06FfECDa7363E0B96684", "type": "string" }, - "positionAddress": { "description": "NFT token ID of the position", "type": "string" }, - "baseTokenAmount": { "description": "Amount of base token to add", "type": "number" }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number" }, + "positionAddress": { + "description": "NFT token ID of the position", + "type": "string" + }, + "baseTokenAmount": { + "description": "Amount of base token to add", + "type": "number" + }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number" + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -13272,14 +17848,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -13302,10 +17889,26 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "bsc", "example": "bsc" }, - "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, - "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" }, - "percentageToRemove": { "type": "number", "minimum": 0, "maximum": 100, "example": 50 } + "network": { + "type": "string", + "default": "bsc", + "example": "bsc" + }, + "walletAddress": { + "type": "string", + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" + }, + "positionAddress": { + "type": "string", + "description": "Position NFT token ID", + "example": "1234" + }, + "percentageToRemove": { + "type": "number", + "minimum": 0, + "maximum": 100, + "example": 50 + } }, "required": ["positionAddress", "percentageToRemove"] } @@ -13321,14 +17924,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -13351,9 +17965,20 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string", "default": "bsc", "example": "bsc" }, - "walletAddress": { "type": "string", "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" }, - "positionAddress": { "type": "string", "description": "Position NFT token ID", "example": "1234" } + "network": { + "type": "string", + "default": "bsc", + "example": "bsc" + }, + "walletAddress": { + "type": "string", + "example": "0xDA50C69342216b538Daf06FfECDa7363E0B96684" + }, + "positionAddress": { + "type": "string", + "description": "Position NFT token ID", + "example": "1234" + } }, "required": ["positionAddress"] } @@ -13369,14 +17994,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -13399,9 +18035,15 @@ "schema": { "type": "object", "properties": { - "network": { "type": "string" }, - "walletAddress": { "type": "string" }, - "positionAddress": { "type": "string" } + "network": { + "type": "string" + }, + "walletAddress": { + "type": "string" + }, + "positionAddress": { + "type": "string" + } }, "required": ["positionAddress"] } @@ -13417,17 +18059,34 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "positionRentRefunded": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": [ "fee", @@ -13504,14 +18163,30 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was initialized at (quote per base)", "type": "number" }, - "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + "data": { + "type": "object", + "properties": { + "fee": { + "type": "number" + } + }, + "required": ["fee"] + } }, "required": ["signature", "status", "poolAddress"] } @@ -13527,14 +18202,20 @@ "description": "Get CLMM pool information from PancakeSwap Solana", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", "name": "poolAddress", @@ -13542,7 +18223,12 @@ "description": "PancakeSwap CLMM pool address" }, { - "schema": { "default": 0, "minimum": 0, "maximum": 401, "type": "integer" }, + "schema": { + "default": 0, + "minimum": 0, + "maximum": 401, + "type": "integer" + }, "in": "query", "name": "binCount", "required": false, @@ -13557,24 +18243,50 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "binStep": { "type": "number" }, - "feePct": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "activeBinId": { "type": "number" }, + "address": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "binStep": { + "type": "number" + }, + "feePct": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "activeBinId": { + "type": "number" + }, "bins": { "type": "array", "items": { "type": "object", "properties": { - "binId": { "type": "number" }, - "price": { "type": "number" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" } + "binId": { + "type": "number" + }, + "price": { + "type": "number" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + } }, "required": ["binId", "price", "baseTokenAmount", "quoteTokenAmount"] } @@ -13603,14 +18315,20 @@ "description": "Get CLMM position information from PancakeSwap Solana", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "", "in": "query", "name": "positionAddress", @@ -13626,19 +18344,45 @@ "schema": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -13668,14 +18412,20 @@ "description": "Retrieve all positions owned by a user's wallet across all PancakeSwap Solana CLMM pools", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "in": "query", "name": "walletAddress", @@ -13683,7 +18433,9 @@ "description": "Solana wallet address to check for positions" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", "name": "poolAddress", @@ -13701,19 +18453,45 @@ "items": { "type": "object", "properties": { - "address": { "type": "string" }, - "poolAddress": { "type": "string" }, - "baseTokenAddress": { "type": "string" }, - "quoteTokenAddress": { "type": "string" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseFeeAmount": { "type": "number" }, - "quoteFeeAmount": { "type": "number" }, - "lowerBinId": { "type": "number" }, - "upperBinId": { "type": "number" }, - "lowerPrice": { "type": "number" }, - "upperPrice": { "type": "number" }, - "price": { "type": "number" } + "address": { + "type": "string" + }, + "poolAddress": { + "type": "string" + }, + "baseTokenAddress": { + "type": "string" + }, + "quoteTokenAddress": { + "type": "string" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseFeeAmount": { + "type": "number" + }, + "quoteFeeAmount": { + "type": "number" + }, + "lowerBinId": { + "type": "number" + }, + "upperBinId": { + "type": "number" + }, + "lowerPrice": { + "type": "number" + }, + "upperPrice": { + "type": "number" + }, + "price": { + "type": "number" + } }, "required": [ "address", @@ -13744,14 +18522,20 @@ "description": "Quote position amounts for PancakeSwap Solana CLMM (simplified)", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", "name": "poolAddress", @@ -13759,7 +18543,9 @@ "description": "PancakeSwap CLMM pool address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 150, "in": "query", "name": "lowerPrice", @@ -13767,7 +18553,9 @@ "description": "Lower price bound for the position" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 250, "in": "query", "name": "upperPrice", @@ -13775,7 +18563,9 @@ "description": "Upper price bound for the position" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "baseTokenAmount", @@ -13783,7 +18573,9 @@ "description": "Amount of base token to deposit" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 2, "in": "query", "name": "quoteTokenAmount", @@ -13791,7 +18583,12 @@ "description": "Amount of quote token to deposit" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -13807,11 +18604,21 @@ "schema": { "type": "object", "properties": { - "baseLimited": { "type": "boolean" }, - "baseTokenAmount": { "type": "number" }, - "quoteTokenAmount": { "type": "number" }, - "baseTokenAmountMax": { "type": "number" }, - "quoteTokenAmountMax": { "type": "number" }, + "baseLimited": { + "type": "boolean" + }, + "baseTokenAmount": { + "type": "number" + }, + "quoteTokenAmount": { + "type": "number" + }, + "baseTokenAmountMax": { + "type": "number" + }, + "quoteTokenAmountMax": { + "type": "number" + }, "liquidity": {} }, "required": [ @@ -13834,14 +18641,20 @@ "description": "Get swap quote for PancakeSwap Solana CLMM with fee and estimated price impact based on pool liquidity", "parameters": [ { - "schema": { "default": "mainnet-beta", "enum": ["devnet", "mainnet-beta"], "type": "string" }, + "schema": { + "default": "mainnet-beta", + "enum": ["devnet", "mainnet-beta"], + "type": "string" + }, "in": "query", "name": "network", "required": false, "description": "Solana network to use" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN", "in": "query", "name": "poolAddress", @@ -13849,7 +18662,9 @@ "description": "CLMM pool address (optional - can be looked up from tokens)" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "SOL", "in": "query", "name": "baseToken", @@ -13857,7 +18672,9 @@ "description": "Base token symbol or address" }, { - "schema": { "type": "string" }, + "schema": { + "type": "string" + }, "example": "USDC", "in": "query", "name": "quoteToken", @@ -13865,7 +18682,9 @@ "description": "Quote token symbol or address" }, { - "schema": { "type": "number" }, + "schema": { + "type": "number" + }, "example": 0.01, "in": "query", "name": "amount", @@ -13873,14 +18692,23 @@ "description": "Amount to swap" }, { - "schema": { "enum": ["BUY", "SELL"], "default": "SELL", "type": "string" }, + "schema": { + "enum": ["BUY", "SELL"], + "default": "SELL", + "type": "string" + }, "in": "query", "name": "side", "required": true, "description": "Trade direction" }, { - "schema": { "minimum": 0, "maximum": 100, "default": 2, "type": "number" }, + "schema": { + "minimum": 0, + "maximum": 100, + "default": 2, + "type": "number" + }, "example": 2, "in": "query", "name": "slippagePct", @@ -13896,16 +18724,36 @@ "schema": { "type": "object", "properties": { - "poolAddress": { "type": "string" }, - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "price": { "type": "number" }, - "slippagePct": { "type": "number" }, - "minAmountOut": { "type": "number" }, - "maxAmountIn": { "type": "number" }, - "priceImpactPct": { "type": "number" } + "poolAddress": { + "type": "string" + }, + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "price": { + "type": "number" + }, + "slippagePct": { + "type": "number" + }, + "minAmountOut": { + "type": "number" + }, + "maxAmountIn": { + "type": "number" + }, + "priceImpactPct": { + "type": "number" + } }, "required": [ "poolAddress", @@ -13951,9 +18799,21 @@ "type": "string", "example": "4QU2NpRaqmKMvPSwVKQDeW4V6JFEKJdkzbzdauumD9qN" }, - "baseToken": { "description": "Base token symbol or address", "type": "string", "example": "SOL" }, - "quoteToken": { "description": "Quote token symbol or address", "type": "string", "example": "USDC" }, - "amount": { "description": "Amount to swap", "type": "number", "example": 0.01 }, + "baseToken": { + "description": "Base token symbol or address", + "type": "string", + "example": "SOL" + }, + "quoteToken": { + "description": "Quote token symbol or address", + "type": "string", + "example": "USDC" + }, + "amount": { + "description": "Amount to swap", + "type": "number", + "example": 0.01 + }, "side": { "description": "Trade direction", "enum": ["BUY", "SELL"], @@ -13984,18 +18844,41 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "tokenIn": { "type": "string" }, - "tokenOut": { "type": "string" }, - "amountIn": { "type": "number" }, - "amountOut": { "type": "number" }, - "fee": { "type": "number" }, - "baseTokenBalanceChange": { "type": "number" }, - "quoteTokenBalanceChange": { "type": "number" } + "tokenIn": { + "type": "string" + }, + "tokenOut": { + "type": "string" + }, + "amountIn": { + "type": "number" + }, + "amountOut": { + "type": "number" + }, + "fee": { + "type": "number" + }, + "baseTokenBalanceChange": { + "type": "number" + }, + "quoteTokenBalanceChange": { + "type": "number" + }, + "slippagePct": { + "description": "Slippage tolerance percentage actually applied to the swap", + "type": "number" + } }, "required": [ "tokenIn", @@ -14074,14 +18957,30 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, - "poolAddress": { "description": "Address of the newly created pool", "type": "string" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, + "poolAddress": { + "description": "Address of the newly created pool", + "type": "string" + }, "price": { "description": "Initial price the pool was initialized at (quote per base)", "type": "number" }, - "data": { "type": "object", "properties": { "fee": { "type": "number" } }, "required": ["fee"] } + "data": { + "type": "object", + "properties": { + "fee": { + "type": "number" + } + }, + "required": ["fee"] + } }, "required": ["signature", "status", "poolAddress"] } @@ -14160,16 +19059,31 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionAddress": { "type": "string" }, - "positionRent": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "positionAddress": { + "type": "string" + }, + "positionRent": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": [ "fee", @@ -14209,13 +19123,21 @@ "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "positionAddress": { "description": "Position NFT address", "type": "string", "example": "" }, + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + }, "baseTokenAmount": { "description": "Amount of base token to add", "type": "number", "example": 0.01 }, - "quoteTokenAmount": { "description": "Amount of quote token to add", "type": "number", "example": 2 }, + "quoteTokenAmount": { + "description": "Amount of quote token to add", + "type": "number", + "example": 2 + }, "slippagePct": { "minimum": 0, "maximum": 100, @@ -14239,14 +19161,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountAdded": { "type": "number" }, - "quoteTokenAmountAdded": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountAdded": { + "type": "number" + }, + "quoteTokenAmountAdded": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountAdded", "quoteTokenAmountAdded"] } @@ -14307,14 +19240,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" } + "fee": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + } }, "required": ["fee", "baseTokenAmountRemoved", "quoteTokenAmountRemoved"] } @@ -14330,7 +19274,7 @@ "/connectors/pancakeswap-sol/clmm/collect-fees": { "post": { "tags": ["/connector/pancakeswap-sol"], - "description": "Collect accumulated fees from a PancakeSwap Solana CLMM position (removes 1% liquidity)", + "description": "Collect accumulated fees from a PancakeSwap Solana CLMM position (zero-liquidity decrease; liquidity is not touched)", "requestBody": { "content": { "application/json": { @@ -14348,7 +19292,11 @@ "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "positionAddress": { "description": "Position NFT address", "type": "string", "example": "" } + "positionAddress": { + "description": "Position NFT address", + "type": "string", + "example": "" + } }, "required": ["positionAddress"] } @@ -14364,14 +19312,25 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": ["fee", "baseFeeAmountCollected", "quoteFeeAmountCollected"] } @@ -14405,7 +19364,11 @@ "default": "82SggYRE2Vo4jN4a2pk3aQ4SET4ctafZJGbowmCqyHx5", "type": "string" }, - "positionAddress": { "description": "Position NFT address to close", "type": "string", "example": "" } + "positionAddress": { + "description": "Position NFT address to close", + "type": "string", + "example": "" + } }, "required": ["positionAddress"] } @@ -14421,17 +19384,34 @@ "schema": { "type": "object", "properties": { - "signature": { "type": "string" }, - "status": { "description": "TransactionStatus enum value", "type": "number" }, + "signature": { + "type": "string" + }, + "status": { + "description": "TransactionStatus enum value", + "type": "number" + }, "data": { "type": "object", "properties": { - "fee": { "type": "number" }, - "positionRentRefunded": { "type": "number" }, - "baseTokenAmountRemoved": { "type": "number" }, - "quoteTokenAmountRemoved": { "type": "number" }, - "baseFeeAmountCollected": { "type": "number" }, - "quoteFeeAmountCollected": { "type": "number" } + "fee": { + "type": "number" + }, + "positionRentRefunded": { + "type": "number" + }, + "baseTokenAmountRemoved": { + "type": "number" + }, + "quoteTokenAmountRemoved": { + "type": "number" + }, + "baseFeeAmountCollected": { + "type": "number" + }, + "quoteFeeAmountCollected": { + "type": "number" + } }, "required": [ "fee", @@ -14452,27 +19432,91 @@ } } }, - "servers": [{ "url": "http://localhost:15889" }], + "servers": [ + { + "url": "http://localhost:15889" + } + ], "tags": [ - { "name": "/config", "description": "System configuration endpoints" }, - { "name": "/wallet", "description": "Wallet management endpoints" }, - { "name": "/tokens", "description": "Token management endpoints" }, - { "name": "/pools", "description": "Pool management endpoints" }, - { "name": "/trading/swap", "description": "Unified cross-chain swap endpoints" }, - { "name": "/trading/clmm", "description": "Unified cross-chain CLMM (Concentrated Liquidity) endpoints" }, - { "name": "/trading/amm", "description": "Unified cross-connector AMM endpoints (pool creation)" }, - { "name": "/chain/solana", "description": "Solana and SVM-based chain endpoints" }, - { "name": "/chain/ethereum", "description": "Ethereum and EVM-based chain endpoints" }, - { "name": "/connector/jupiter", "description": "Jupiter connector endpoints" }, - { "name": "/connector/meteora", "description": "Meteora connector endpoints" }, - { "name": "/connector/orca", "description": "Orca connector endpoints" }, - { "name": "/connector/raydium", "description": "Raydium connector endpoints" }, - { "name": "/connector/uniswap", "description": "Uniswap connector endpoints" }, - { "name": "/connector/0x", "description": "0x connector endpoints" }, - { "name": "/connector/pancakeswap-sol", "description": "PancakeSwap Solana connector endpoints" }, - { "name": "/connector/pancakeswap", "description": "PancakeSwap EVM connector endpoints" }, - { "name": "/connector/dflow", "description": "DFlow connector endpoints" }, - { "name": "/connector/okx", "description": "OKX DEX aggregator connector endpoints" }, - { "name": "/connector/titan", "description": "Titan connector endpoints" } + { + "name": "/config", + "description": "System configuration endpoints" + }, + { + "name": "/wallet", + "description": "Wallet management endpoints" + }, + { + "name": "/tokens", + "description": "Token management endpoints" + }, + { + "name": "/pools", + "description": "Pool management endpoints" + }, + { + "name": "/trading/swap", + "description": "Unified cross-chain swap endpoints" + }, + { + "name": "/trading/clmm", + "description": "Unified cross-chain CLMM (Concentrated Liquidity) endpoints" + }, + { + "name": "/trading/amm", + "description": "Unified cross-connector AMM endpoints (pool creation)" + }, + { + "name": "/chain/solana", + "description": "Solana and SVM-based chain endpoints" + }, + { + "name": "/chain/ethereum", + "description": "Ethereum and EVM-based chain endpoints" + }, + { + "name": "/connector/jupiter", + "description": "Jupiter connector endpoints" + }, + { + "name": "/connector/meteora", + "description": "Meteora connector endpoints" + }, + { + "name": "/connector/orca", + "description": "Orca connector endpoints" + }, + { + "name": "/connector/raydium", + "description": "Raydium connector endpoints" + }, + { + "name": "/connector/uniswap", + "description": "Uniswap connector endpoints" + }, + { + "name": "/connector/0x", + "description": "0x connector endpoints" + }, + { + "name": "/connector/pancakeswap-sol", + "description": "PancakeSwap Solana connector endpoints" + }, + { + "name": "/connector/pancakeswap", + "description": "PancakeSwap EVM connector endpoints" + }, + { + "name": "/connector/dflow", + "description": "DFlow connector endpoints" + }, + { + "name": "/connector/okx", + "description": "OKX DEX aggregator connector endpoints" + }, + { + "name": "/connector/titan", + "description": "Titan connector endpoints" + } ] } From b5f5e04f585a6309c4d7ee59bb191db5473459c5 Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 23:16:04 -0700 Subject: [PATCH 30/31] fix(raydium): collect fees without withdrawing liquidity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- .../raydium/clmm-routes/collectFees.ts | 89 +++++++++++-------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/src/connectors/raydium/clmm-routes/collectFees.ts b/src/connectors/raydium/clmm-routes/collectFees.ts index fd26cbe2a3..9bace858ce 100644 --- a/src/connectors/raydium/clmm-routes/collectFees.ts +++ b/src/connectors/raydium/clmm-routes/collectFees.ts @@ -1,4 +1,6 @@ +import { TxVersion } from '@raydium-io/raydium-sdk-v2'; import { PublicKey } from '@solana/web3.js'; +import BN from 'bn.js'; import { FastifyPluginAsync } from 'fastify'; import { Solana } from '../../../chains/solana/solana'; @@ -12,8 +14,15 @@ import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Raydium } from '../raydium'; -import { removeLiquidity } from './removeLiquidity'; - +/** + * Collect accumulated fees from a position WITHOUT touching its liquidity. + * + * The Raydium CLMM program has no owner-facing "collect fees" instruction — fees owed + * to a position are transferred by decrease_liquidity. Calling it with liquidity = 0 + * collects what is owed and leaves the position intact. This route used to remove 1% + * of the position and report the withdrawn principal as fees, which both mutated the + * position and mis-stated the amounts. + */ export async function collectFees( network: string, walletAddress: string, @@ -23,8 +32,7 @@ export async function collectFees( const raydium = await Raydium.getInstance(network); // Set the SDK owner to the wallet's public key — works for every wallet type (local, - // hardware). The actual liquidity removal (and signing/sending) is delegated to - // removeLiquidity below. + // hardware). The tx is built unsigned; signing/sending is delegated below. await raydium.setOwner(new PublicKey(walletAddress)); const position = await raydium.getClmmPosition(positionAddress); @@ -32,58 +40,69 @@ export async function collectFees( throw httpErrors.notFound(`Position not found: ${positionAddress}`); } - const [poolInfo] = await raydium.getClmmPoolfromAPI(position.poolId.toBase58()); + const [poolInfo, poolKeys] = await raydium.getClmmPoolfromAPI(position.poolId.toBase58()); const tokenA = await solana.getToken(poolInfo.mintA.address); const tokenB = await solana.getToken(poolInfo.mintB.address); - logger.info(`Collecting fees from CLMM position ${positionAddress} by removing 1% liquidity`); + logger.info(`Collecting fees from CLMM position ${positionAddress} via zero-liquidity decrease`); - // Remove 1% of liquidity to collect fees - const removeLiquidityResponse = await removeLiquidity( - network, - walletAddress, - positionAddress, - 1, // 1% of position - false, // don't close position - ); + const COMPUTE_UNITS = 600000; + const priorityFeeInLamports = await solana.estimateGasPrice(); + const priorityFeePerCU = Math.floor(priorityFeeInLamports * 1e6); + + const { transaction } = await raydium.raydiumSDK.clmm.decreaseLiquidity({ + poolInfo, + poolKeys, + ownerPosition: position, + ownerInfo: { + useSOLBalance: true, + closePosition: false, + }, + liquidity: new BN(0), // collect fees only; principal untouched + amountMinA: new BN(0), + amountMinB: new BN(0), + txVersion: TxVersion.V0, + computeBudgetConfig: { + units: COMPUTE_UNITS, + microLamports: priorityFeePerCU, + }, + }); - if (removeLiquidityResponse.status === 1 && removeLiquidityResponse.data) { - // Use the new helper to extract balance changes including fees + const { signature } = await solana.sendAndConfirmTransactionForWallet(transaction, walletAddress); + // Retrying re-fetch; throws the shared landed-but-failed error if the transaction + // landed with an error, so txData existing below really means "confirmed". + const txData = await solana.getConfirmedTransactionData(signature); + + if (txData) { + // No liquidity moved, so the whole balance change is fees. const { baseTokenChange, quoteTokenChange } = await solana.extractClmmBalanceChanges( - removeLiquidityResponse.signature, + signature, walletAddress, tokenA, tokenB, - removeLiquidityResponse.data.fee * 1e9, + txData.meta.fee, ); - // The total balance change includes both liquidity removal and fee collection - // Since we know the liquidity amounts from removeLiquidity response, - // we can calculate the fee amounts - const baseFeeCollected = Math.abs(baseTokenChange) - removeLiquidityResponse.data.baseTokenAmountRemoved; - const quoteFeeCollected = Math.abs(quoteTokenChange) - removeLiquidityResponse.data.quoteTokenAmountRemoved; - logger.info( - `Fees collected from position ${positionAddress}: ${Math.max(0, baseFeeCollected).toFixed(4)} ${tokenA.symbol}, ${Math.max(0, quoteFeeCollected).toFixed(4)} ${tokenB.symbol}`, + `Fees collected from position ${positionAddress}: ${Math.abs(baseTokenChange).toFixed(4)} ${tokenA.symbol}, ${Math.abs(quoteTokenChange).toFixed(4)} ${tokenB.symbol}`, ); return { - signature: removeLiquidityResponse.signature, + signature, status: 1, // CONFIRMED data: { - fee: removeLiquidityResponse.data.fee, - baseFeeAmountCollected: Math.max(0, baseFeeCollected), - quoteFeeAmountCollected: Math.max(0, quoteFeeCollected), + fee: txData.meta.fee / 1e9, + baseFeeAmountCollected: Math.abs(baseTokenChange), + quoteFeeAmountCollected: Math.abs(quoteTokenChange), }, }; - } else { - // Return pending status - return { - signature: removeLiquidityResponse.signature, - status: removeLiquidityResponse.status, - }; } + + return { + signature, + status: 0, // PENDING + }; } export const collectFeesRoute: FastifyPluginAsync = async (fastify) => { From fdad604aa708f88215988688f80401ec70b6f53c Mon Sep 17 00:00:00 2001 From: Michael Feng Date: Tue, 18 Aug 2026 23:42:33 -0700 Subject: [PATCH 31/31] fix(evm): route every EVM transaction through one confirmation gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 828159e63: - 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 Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr --- src/chains/ethereum/ethereum.ts | 64 +++++++ src/chains/ethereum/routes/approve.ts | 57 +++--- src/chains/ethereum/routes/unwrap.ts | 32 ++-- src/chains/ethereum/routes/wrap.ts | 32 ++-- .../pancakeswap/amm-routes/addLiquidity.ts | 19 +- .../pancakeswap/amm-routes/createPool.ts | 28 +-- .../pancakeswap/amm-routes/executeSwap.ts | 36 ++-- .../pancakeswap/amm-routes/removeLiquidity.ts | 15 +- .../pancakeswap/clmm-routes/addLiquidity.ts | 14 +- .../pancakeswap/clmm-routes/closePosition.ts | 14 +- .../pancakeswap/clmm-routes/collectFees.ts | 14 +- .../pancakeswap/clmm-routes/createPool.ts | 24 +-- .../pancakeswap/clmm-routes/executeSwap.ts | 36 ++-- .../pancakeswap/clmm-routes/openPosition.ts | 26 ++- .../clmm-routes/removeLiquidity.ts | 14 +- .../uniswap/amm-routes/addLiquidity.ts | 19 +- .../uniswap/amm-routes/createPool.ts | 28 +-- .../uniswap/amm-routes/executeSwap.ts | 36 ++-- .../uniswap/amm-routes/removeLiquidity.ts | 15 +- .../uniswap/clmm-routes/addLiquidity.ts | 14 +- .../uniswap/clmm-routes/closePosition.ts | 16 +- .../uniswap/clmm-routes/collectFees.ts | 16 +- .../uniswap/clmm-routes/createPool.ts | 24 +-- .../uniswap/clmm-routes/executeSwap.ts | 36 ++-- .../uniswap/clmm-routes/openPosition.ts | 31 +++- .../uniswap/clmm-routes/removeLiquidity.ts | 16 +- .../ethereum/transaction-confirmation.test.ts | 83 +++++++++ .../remove-liquidity-confirmation.test.ts | 140 ++++++++++++++ .../collect-fees-confirmation.test.ts | 143 +++++++++++++++ .../open-position-confirmation.test.ts | 171 ++++++++++++++++++ 30 files changed, 925 insertions(+), 288 deletions(-) create mode 100644 test/chains/ethereum/transaction-confirmation.test.ts create mode 100644 test/connectors/uniswap/amm-routes/remove-liquidity-confirmation.test.ts create mode 100644 test/connectors/uniswap/clmm-routes/collect-fees-confirmation.test.ts create mode 100644 test/connectors/uniswap/clmm-routes/open-position-confirmation.test.ts diff --git a/src/chains/ethereum/ethereum.ts b/src/chains/ethereum/ethereum.ts index 9e2d8a92df..490b469b49 100644 --- a/src/chains/ethereum/ethereum.ts +++ b/src/chains/ethereum/ethereum.ts @@ -10,6 +10,7 @@ import { RPCProvider } from '../../rpc/rpc-provider-base'; import { TokenValue, tokenValueToString } from '../../services/base'; import { ConfigManagerCertPassphrase } from '../../services/config-manager-cert-passphrase'; import { ConfigManagerV2 } from '../../services/config-manager-v2'; +import { transactionFailed } from '../../services/error-handler'; import { logger, redactUrl } from '../../services/logger'; import { TokenService } from '../../services/token-service'; import { walletPath, isHardwareWallet as checkIsHardwareWallet } from '../../wallet/utils'; @@ -29,6 +30,15 @@ export interface TokenInfo { export type NewBlockHandler = (bn: number) => void; export type NewDebugMsgHandler = (msg: any) => void; +/** + * Outcome of an EVM liquidity/pool transaction as a route is allowed to report it. + * A revert is not one of the cases: it throws, so it can never be mistaken for PENDING. + * See {@link Ethereum.handleTransactionConfirmation}. + */ +export type EthereumTransactionOutcome = + | { confirmed: false; signature: string } + | { confirmed: true; signature: string; receipt: providers.TransactionReceipt; fee: number }; + // Networks that support EIP-1559 (type 2) transactions export const EIP1559_NETWORKS = [ 'mainnet', @@ -1026,6 +1036,60 @@ export class Ethereum { return null; } + /** + * Single confirmation gate for EVM liquidity and pool transactions (open/close position, + * add/remove liquidity, collect fees, create pool, execute swap). + * + * `handleTransactionExecution` has three outcomes but only two of them are a valid response + * body, so every caller used to have to remember two separate checks — and almost none did: + * + * - no receipt (still pending after the extended poll) — dereferencing it throws a TypeError + * that the route catch turns into a generic 500, losing the transaction hash and with it + * any chance of reconciling a transaction that lands a minute later. + * - `receipt.status === 0` (reverted on-chain) — forwarding it verbatim as the response + * `status` reports the revert as {@link TransactionStatus.PENDING}, which is also 0, so a + * poller waits on it forever while the route's pre-send amounts are booked as if the + * tokens had moved. + * + * This helper resolves both: + * + * - still pending -> `{ confirmed: false, signature: tx.hash }`. The caller returns + * `{ signature, status: TransactionStatus.PENDING }` with NO `data` — the amounts it + * computed before sending have not moved and must not be reported as if they had. + * - reverted -> throws the shared 400 TRANSACTION_FAILED, the same terminal, non-retryable + * error the Solana routes throw for a landed-but-failed transaction. + * - confirmed -> `{ confirmed: true, signature, receipt, fee }`, fee already converted from + * gas units to the chain's native currency. + */ + public async handleTransactionConfirmation(tx: TransactionResponse): Promise { + const receipt = await this.handleTransactionExecution(tx); + + if (!receipt) { + logger.warn(`Transaction ${tx.hash} still pending — reporting PENDING so the caller can reconcile it later`); + return { confirmed: false, signature: tx.hash }; + } + + if (receipt.status === 0) { + throw transactionFailed( + `Transaction ${receipt.transactionHash} reverted on-chain. Gas was spent; no tokens moved.`, + ); + } + + if (receipt.status !== 1) { + // No status on the receipt (pre-Byzantium chain or a provider quirk): neither a + // confirmation nor a revert, so report it as pending rather than guessing. + logger.warn(`Transaction ${receipt.transactionHash} has no receipt status — reporting PENDING`); + return { confirmed: false, signature: receipt.transactionHash }; + } + + return { + confirmed: true, + signature: receipt.transactionHash, + receipt, + fee: parseFloat(utils.formatUnits(receipt.gasUsed.mul(receipt.effectiveGasPrice), 18)), + }; + } + /** * Handle transaction confirmation status and return appropriate response * Similar to Solana's handleConfirmation helper diff --git a/src/chains/ethereum/routes/approve.ts b/src/chains/ethereum/routes/approve.ts index 026b6be472..2e6107b7ea 100644 --- a/src/chains/ethereum/routes/approve.ts +++ b/src/chains/ethereum/routes/approve.ts @@ -3,6 +3,7 @@ import { FastifyPluginAsync, FastifyInstance } from 'fastify'; import { getSpender as pancakeswapSpender } from '../../../connectors/pancakeswap/pancakeswap.contracts'; import { getSpender as uniswapSpender } from '../../../connectors/uniswap/uniswap.contracts'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { bigNumberWithDecimalToStr } from '../../../services/base'; import { logger } from '../../../services/logger'; import { Ethereum } from '../ethereum'; @@ -178,17 +179,20 @@ export async function approveEthereumToken( const txResponse = await ethereum.provider.sendTransaction(signedTx); // Wait for confirmation with timeout - const receipt = await ethereum.handleTransactionExecution(txResponse); + // A revert throws 400 TRANSACTION_FAILED out of the helper; only a still-pending + // transaction comes back unconfirmed, and an approval cannot be reported without a + // confirmed allowance. + const outcome = await ethereum.handleTransactionConfirmation(txResponse); - if (!receipt || receipt.status === -1) { + if (!outcome.confirmed) { throw new Error('Transaction timed out or failed to get receipt'); } approval = { - hash: receipt.transactionHash, + hash: outcome.signature, nonce: nonce, - gasUsed: receipt.gasUsed, - effectiveGasPrice: receipt.effectiveGasPrice, + gasUsed: outcome.receipt.gasUsed, + effectiveGasPrice: outcome.receipt.effectiveGasPrice, }; } else { // Regular wallet flow @@ -207,18 +211,17 @@ export async function approveEthereumToken( const tx = await ethereum.approveERC20(contract, wallet, spenderAddress, amountBigNumber); // Wait for the transaction to be mined with timeout (60 seconds for approvals) - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); - if (!receipt || receipt.status === -1) { + if (!outcome.confirmed) { throw new Error('Transaction timed out or failed to get receipt'); } approval = { hash: tx.hash, nonce: tx.nonce, - gasUsed: receipt.gasUsed, - effectiveGasPrice: receipt.effectiveGasPrice, - status: receipt.status, + gasUsed: outcome.receipt.gasUsed, + effectiveGasPrice: outcome.receipt.effectiveGasPrice, }; } } else { @@ -229,7 +232,6 @@ export async function approveEthereumToken( nonce: 0, gasUsed: ethers.BigNumber.from('0'), effectiveGasPrice: ethers.BigNumber.from('0'), - status: 1, }; } @@ -291,20 +293,18 @@ export async function approveEthereumToken( const txResponse = await ethereum.provider.sendTransaction(signedTx); // Wait for confirmation with extended timeout - const permit2Receipt = await ethereum.handleTransactionExecution(txResponse); + const permit2Outcome = await ethereum.handleTransactionConfirmation(txResponse); - if (!permit2Receipt) { + if (!permit2Outcome.confirmed) { throw new Error('Permit2 transaction timed out or failed to get receipt'); } - logger.info(`Permit2 approval transaction confirmed: ${permit2Receipt.transactionHash}`); + logger.info(`Permit2 approval transaction confirmed: ${permit2Outcome.signature}`); // Update fee to include both transactions - if (permit2Receipt.gasUsed && permit2Receipt.effectiveGasPrice) { - const permit2FeeInWei = permit2Receipt.gasUsed.mul(permit2Receipt.effectiveGasPrice); - const totalFeeInWei = approval.gasUsed.mul(approval.effectiveGasPrice).add(permit2FeeInWei); - feeInEth = utils.formatEther(totalFeeInWei); - } + const permit2FeeInWei = permit2Outcome.receipt.gasUsed.mul(permit2Outcome.receipt.effectiveGasPrice); + const totalFeeInWei = approval.gasUsed.mul(approval.effectiveGasPrice).add(permit2FeeInWei); + feeInEth = utils.formatEther(totalFeeInWei); } else { // Regular wallet flow for Permit2 approve const wallet = await ethereum.getWallet(address); @@ -326,20 +326,18 @@ export async function approveEthereumToken( ); // Wait for confirmation with extended timeout - const permit2Receipt = await ethereum.handleTransactionExecution(permit2Tx); + const permit2Outcome = await ethereum.handleTransactionConfirmation(permit2Tx); - if (!permit2Receipt || permit2Receipt.status === -1) { + if (!permit2Outcome.confirmed) { throw new Error('Permit2 transaction timed out or failed to get receipt'); } - logger.info(`Permit2 approval transaction confirmed: ${permit2Receipt.transactionHash}`); + logger.info(`Permit2 approval transaction confirmed: ${permit2Outcome.signature}`); // Update fee to include both transactions - if (permit2Receipt.gasUsed && permit2Receipt.effectiveGasPrice) { - const permit2FeeInWei = permit2Receipt.gasUsed.mul(permit2Receipt.effectiveGasPrice); - const totalFeeInWei = approval.gasUsed.mul(approval.effectiveGasPrice).add(permit2FeeInWei); - feeInEth = utils.formatEther(totalFeeInWei); - } + const permit2FeeInWei = permit2Outcome.receipt.gasUsed.mul(permit2Outcome.receipt.effectiveGasPrice); + const totalFeeInWei = approval.gasUsed.mul(approval.effectiveGasPrice).add(permit2FeeInWei); + feeInEth = utils.formatEther(totalFeeInWei); } logger.info( @@ -349,7 +347,10 @@ export async function approveEthereumToken( return { signature: approval.hash, - status: approval.status ?? -1, + // Every path that reaches here confirmed: the helper throws on a revert and the + // branches above bail out while a transaction is still pending. `approval.status ?? -1` + // used to report a confirmed Ledger approval (which never carried a status) as FAILED. + status: TransactionStatus.CONFIRMED, data: { tokenAddress: fullToken.address, spender: isUniversalRouter ? universalRouterAddress || spenderAddress : spenderAddress, diff --git a/src/chains/ethereum/routes/unwrap.ts b/src/chains/ethereum/routes/unwrap.ts index 54001c7a03..05c939b282 100644 --- a/src/chains/ethereum/routes/unwrap.ts +++ b/src/chains/ethereum/routes/unwrap.ts @@ -1,9 +1,10 @@ import { ethers, utils } from 'ethers'; import { FastifyPluginAsync, FastifyInstance } from 'fastify'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { bigNumberWithDecimalToStr } from '../../../services/base'; import { logger } from '../../../services/logger'; -import { Ethereum } from '../ethereum'; +import { Ethereum, EthereumTransactionOutcome } from '../ethereum'; import { EthereumLedger } from '../ethereum-ledger'; import { UnwrapRequestSchema, UnwrapResponseSchema, UnwrapRequestType, UnwrapResponseType } from '../schemas'; @@ -72,9 +73,9 @@ export async function unwrapEthereum(fastify: FastifyInstance, network: string, const amountInWei = utils.parseEther(amount); try { - let transaction; + let transaction: ethers.providers.TransactionResponse; let nonce: number; - let receipt; + let outcome: EthereumTransactionOutcome; if (isHardware) { // Hardware wallet flow @@ -117,12 +118,7 @@ export async function unwrapEthereum(fastify: FastifyInstance, network: string, const txResponse = await ethereum.provider.sendTransaction(signedTx); // Wait for confirmation with timeout - receipt = await ethereum.handleTransactionExecution(txResponse); - - transaction = { - hash: receipt.transactionHash, - nonce: nonce, - }; + outcome = await ethereum.handleTransactionConfirmation(txResponse); } else { // Regular wallet flow let wallet: ethers.Wallet; @@ -157,19 +153,21 @@ export async function unwrapEthereum(fastify: FastifyInstance, network: string, nonce = transaction.nonce; // Wait for transaction confirmation with timeout - receipt = await ethereum.handleTransactionExecution(transaction); + outcome = await ethereum.handleTransactionConfirmation(transaction); } - // Calculate actual fee from receipt - let feeInEth = '0'; - if (receipt.gasUsed && receipt.effectiveGasPrice) { - const feeInWei = receipt.gasUsed.mul(receipt.effectiveGasPrice); - feeInEth = utils.formatEther(feeInWei); + // A revert threw out of the confirmation helper as a 400 TRANSACTION_FAILED — it is never + // reported as PENDING (receipt.status 0 and TransactionStatus.PENDING are the same number). + if (!outcome.confirmed) { + return { signature: outcome.signature, status: TransactionStatus.PENDING }; } + // Calculate actual fee from the confirmed receipt + const feeInEth = utils.formatEther(outcome.receipt.gasUsed.mul(outcome.receipt.effectiveGasPrice)); + return { - signature: transaction.hash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { nonce: nonce, fee: feeInEth, diff --git a/src/chains/ethereum/routes/wrap.ts b/src/chains/ethereum/routes/wrap.ts index b726f9cd6f..a3230047f5 100644 --- a/src/chains/ethereum/routes/wrap.ts +++ b/src/chains/ethereum/routes/wrap.ts @@ -1,9 +1,10 @@ import { ethers, utils } from 'ethers'; import { FastifyPluginAsync, FastifyInstance } from 'fastify'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { bigNumberWithDecimalToStr } from '../../../services/base'; import { logger } from '../../../services/logger'; -import { Ethereum } from '../ethereum'; +import { Ethereum, EthereumTransactionOutcome } from '../ethereum'; import { EthereumLedger } from '../ethereum-ledger'; import { WrapRequestSchema, WrapResponseSchema, WrapRequestType, WrapResponseType } from '../schemas'; @@ -73,9 +74,9 @@ export async function wrapEthereum(fastify: FastifyInstance, network: string, ad const amountInWei = utils.parseEther(amount); try { - let transaction; + let transaction: ethers.providers.TransactionResponse; let nonce: number; - let receipt; + let outcome: EthereumTransactionOutcome; if (isHardware) { // Hardware wallet flow @@ -110,12 +111,7 @@ export async function wrapEthereum(fastify: FastifyInstance, network: string, ad const txResponse = await ethereum.provider.sendTransaction(signedTx); // Wait for confirmation with timeout - receipt = await ethereum.handleTransactionExecution(txResponse); - - transaction = { - hash: receipt.transactionHash, - nonce: nonce, - }; + outcome = await ethereum.handleTransactionConfirmation(txResponse); } else { // Regular wallet flow let wallet: ethers.Wallet; @@ -143,19 +139,21 @@ export async function wrapEthereum(fastify: FastifyInstance, network: string, ad nonce = transaction.nonce; // Wait for transaction confirmation with timeout - receipt = await ethereum.handleTransactionExecution(transaction); + outcome = await ethereum.handleTransactionConfirmation(transaction); } - // Calculate actual fee from receipt - let feeInEth = '0'; - if (receipt.gasUsed && receipt.effectiveGasPrice) { - const feeInWei = receipt.gasUsed.mul(receipt.effectiveGasPrice); - feeInEth = utils.formatEther(feeInWei); + // A revert threw out of the confirmation helper as a 400 TRANSACTION_FAILED — it is never + // reported as PENDING (receipt.status 0 and TransactionStatus.PENDING are the same number). + if (!outcome.confirmed) { + return { signature: outcome.signature, status: TransactionStatus.PENDING }; } + // Calculate actual fee from the confirmed receipt + const feeInEth = utils.formatEther(outcome.receipt.gasUsed.mul(outcome.receipt.effectiveGasPrice)); + return { - signature: transaction.hash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { nonce: nonce, fee: feeInEth, diff --git a/src/connectors/pancakeswap/amm-routes/addLiquidity.ts b/src/connectors/pancakeswap/amm-routes/addLiquidity.ts index c83fac5b1e..074e086a80 100644 --- a/src/connectors/pancakeswap/amm-routes/addLiquidity.ts +++ b/src/connectors/pancakeswap/amm-routes/addLiquidity.ts @@ -7,6 +7,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Ethereum } from '../../../chains/ethereum/ethereum'; import { wrapEthereum } from '../../../chains/ethereum/routes/wrap'; import { AddLiquidityResponseType, AddLiquidityResponse } from '../../../schemas/amm-schema'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Pancakeswap } from '../pancakeswap'; @@ -255,19 +256,17 @@ async function addLiquidityInternal( } // Wait for transaction confirmation - const receipt = await ethereum.handleTransactionExecution(tx); - - // Calculate gas fee - const gasFee = formatTokenAmount( - receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), - 18, // ETH has 18 decimals - ); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the quoted amounts were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseTokenAmountAdded: quote.baseTokenAmount, quoteTokenAmountAdded: quote.quoteTokenAmount, ...(baseWrapTxHash && { baseWrapTxHash }), diff --git a/src/connectors/pancakeswap/amm-routes/createPool.ts b/src/connectors/pancakeswap/amm-routes/createPool.ts index 4d0bb43df7..2bfc343a74 100644 --- a/src/connectors/pancakeswap/amm-routes/createPool.ts +++ b/src/connectors/pancakeswap/amm-routes/createPool.ts @@ -7,6 +7,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Ethereum, TokenInfo } from '../../../chains/ethereum/ethereum'; import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/amm-schema'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { PancakeswapConfig } from '../pancakeswap.config'; @@ -259,32 +260,33 @@ export async function createPool( logger.info(`Creating Pancakeswap V2 pool ${baseTokenInfo.symbol}/${quoteTokenInfo.symbol} via tx ${tx.hash}`); - const receipt = await ethereum.handleTransactionExecution(tx); + // A revert throws out of here (400 TRANSACTION_FAILED) — it is never reported as PENDING. + const outcome = await ethereum.handleTransactionConfirmation(tx); // Read the (now-created) pair address from the factory — authoritative source of the pool address. const pairAddress: string = await factory.getPair(baseTokenInfo.address, quoteTokenInfo.address); - if (receipt && receipt.status === 1) { - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); // ETH has 18 decimals + if (!outcome.confirmed) { + // Timed out but still broadcasting — report PENDING with the tx hash so the caller can + // reconcile it. The seed amounts below have not moved, so they are deliberately omitted. return { - signature: receipt.transactionHash, - status: 1, // CONFIRMED + signature: outcome.signature, + status: TransactionStatus.PENDING, poolAddress: pairAddress, price: seedPrice, - data: { - fee: gasFee, - baseTokenAmountAdded: baseTokenAmount, - quoteTokenAmountAdded: effectiveQuoteAmount, - }, }; } - // Timed out (still broadcasting) or reverted — report as pending with the tx hash. return { - signature: receipt ? receipt.transactionHash : tx.hash, - status: 0, // PENDING + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, poolAddress: pairAddress, price: seedPrice, + data: { + fee: outcome.fee, + baseTokenAmountAdded: baseTokenAmount, + quoteTokenAmountAdded: effectiveQuoteAmount, + }, }; } diff --git a/src/connectors/pancakeswap/amm-routes/executeSwap.ts b/src/connectors/pancakeswap/amm-routes/executeSwap.ts index ba13de036b..15bea8e51a 100644 --- a/src/connectors/pancakeswap/amm-routes/executeSwap.ts +++ b/src/connectors/pancakeswap/amm-routes/executeSwap.ts @@ -1,9 +1,10 @@ import { BigNumber, Contract, utils } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; -import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { Ethereum, EthereumTransactionOutcome } from '../../../chains/ethereum/ethereum'; import { EthereumLedger } from '../../../chains/ethereum/ethereum-ledger'; import { getEthereumChainConfig } from '../../../chains/ethereum/ethereum.config'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { ExecuteSwapRequestType, SwapExecuteResponseType, SwapExecuteResponse } from '../../../schemas/router-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; @@ -94,7 +95,7 @@ export async function executeAmmSwap( // Prepare transaction parameters const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now - let receipt; + let outcome: EthereumTransactionOutcome; try { if (isHardwareWallet) { @@ -159,7 +160,7 @@ export async function executeAmmSwap( logger.info(`Transaction sent: ${txResponse.hash}`); // Wait for confirmation with timeout - receipt = await ethereum.handleTransactionExecution(txResponse); + outcome = await ethereum.handleTransactionConfirmation(txResponse); } else { // Regular wallet flow let wallet; @@ -216,19 +217,18 @@ export async function executeAmmSwap( logger.info(`Transaction sent: ${tx.hash}`); // Wait for transaction confirmation - receipt = await ethereum.handleTransactionExecution(tx); + outcome = await ethereum.handleTransactionConfirmation(tx); } - // Check if the transaction was successful - if (receipt.status === 0) { - logger.error(`Transaction failed on-chain. Receipt: ${JSON.stringify(receipt)}`); - throw httpErrors.internalServerError( - 'Transaction reverted on-chain. This could be due to slippage, insufficient funds, or other blockchain issues.', - ); + // A revert threw out of the confirmation helper as a 400 TRANSACTION_FAILED. What is left + // is a transaction that is still pending after the extended poll: report it as PENDING + // with its hash rather than dereferencing a null receipt and losing the hash to a 500. + if (!outcome.confirmed) { + return { signature: outcome.signature, status: TransactionStatus.PENDING }; } - logger.info(`Transaction confirmed: ${receipt.transactionHash}`); - logger.info(`Gas used: ${receipt.gasUsed.toString()}`); + logger.info(`Transaction confirmed: ${outcome.signature}`); + logger.info(`Gas used: ${outcome.receipt.gasUsed.toString()}`); // Calculate amounts using quote values const amountIn = quote.estimatedAmountIn; @@ -238,25 +238,19 @@ export async function executeAmmSwap( const baseTokenBalanceChange = side === 'BUY' ? amountOut : -amountIn; const quoteTokenBalanceChange = side === 'BUY' ? -amountIn : amountOut; - // Calculate gas fee (formatTokenAmount already returns a number) - const gasFee = formatTokenAmount( - receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), - 18, // ETH has 18 decimals - ); - // Determine token addresses for computed fields const tokenIn = quote.inputToken.address; const tokenOut = quote.outputToken.address; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { tokenIn, tokenOut, amountIn, amountOut, - fee: gasFee, + fee: outcome.fee, baseTokenBalanceChange, quoteTokenBalanceChange, slippagePct, diff --git a/src/connectors/pancakeswap/amm-routes/removeLiquidity.ts b/src/connectors/pancakeswap/amm-routes/removeLiquidity.ts index 6acbd8fc6a..40fd834b47 100644 --- a/src/connectors/pancakeswap/amm-routes/removeLiquidity.ts +++ b/src/connectors/pancakeswap/amm-routes/removeLiquidity.ts @@ -6,6 +6,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Ethereum } from '../../../chains/ethereum/ethereum'; import { RemoveLiquidityResponseType, RemoveLiquidityResponse } from '../../../schemas/amm-schema'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { Pancakeswap } from '../pancakeswap'; @@ -123,16 +124,20 @@ export async function removeLiquidity( ); } - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the expected amounts were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } + const baseTokenAmountRemoved = formatTokenAmount(expectedBaseTokenAmount.toString(), baseTokenObj.decimals); const quoteTokenAmountRemoved = formatTokenAmount(expectedQuoteTokenAmount.toString(), quoteTokenObj.decimals); - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseTokenAmountRemoved, quoteTokenAmountRemoved, }, diff --git a/src/connectors/pancakeswap/clmm-routes/addLiquidity.ts b/src/connectors/pancakeswap/clmm-routes/addLiquidity.ts index 430235e0c2..0081658abd 100644 --- a/src/connectors/pancakeswap/clmm-routes/addLiquidity.ts +++ b/src/connectors/pancakeswap/clmm-routes/addLiquidity.ts @@ -6,6 +6,7 @@ import { BigNumber, utils } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { AddLiquidityResponseType, AddLiquidityResponse } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; @@ -157,9 +158,12 @@ export async function addLiquidity( const txParams = await ethereum.prepareGasOptions(undefined, CLMM_ADD_LIQUIDITY_GAS_LIMIT); txParams.value = BigNumber.from(value.toString()); const tx = await positionManagerWithSigner.multicall([calldata], txParams); - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the mint amounts below were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); const actualToken0Amount = formatTokenAmount(newPosition.mintAmounts.amount0.toString(), token0.decimals); const actualToken1Amount = formatTokenAmount(newPosition.mintAmounts.amount1.toString(), token1.decimals); @@ -167,10 +171,10 @@ export async function addLiquidity( const actualQuoteAmount = isBaseToken0 ? actualToken1Amount : actualToken0Amount; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseTokenAmountAdded: actualBaseAmount, quoteTokenAmountAdded: actualQuoteAmount, }, diff --git a/src/connectors/pancakeswap/clmm-routes/closePosition.ts b/src/connectors/pancakeswap/clmm-routes/closePosition.ts index 1fcbdd567b..8be6a47a84 100644 --- a/src/connectors/pancakeswap/clmm-routes/closePosition.ts +++ b/src/connectors/pancakeswap/clmm-routes/closePosition.ts @@ -6,6 +6,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Address } from 'viem'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { ClosePositionRequestType, ClosePositionRequest, @@ -119,9 +120,12 @@ export async function closePosition( const txParams = await ethereum.prepareGasOptions(undefined, CLMM_CLOSE_POSITION_GAS_LIMIT); txParams.value = BigNumber.from(value.toString()); const tx = await positionManagerWithSigner.multicall([calldata], txParams); - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the amounts below were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); const token0AmountRemoved = formatTokenAmount(totalAmount0.quotient.toString(), token0.decimals); const token1AmountRemoved = formatTokenAmount(totalAmount1.quotient.toString(), token1.decimals); @@ -137,10 +141,10 @@ export async function closePosition( const positionRentRefunded = 0; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, positionRentRefunded, baseTokenAmountRemoved, quoteTokenAmountRemoved, diff --git a/src/connectors/pancakeswap/clmm-routes/collectFees.ts b/src/connectors/pancakeswap/clmm-routes/collectFees.ts index 7366604a0b..0515ed6d37 100644 --- a/src/connectors/pancakeswap/clmm-routes/collectFees.ts +++ b/src/connectors/pancakeswap/clmm-routes/collectFees.ts @@ -6,6 +6,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Address } from 'viem'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { CollectFeesRequestType, CollectFeesRequest, @@ -94,9 +95,12 @@ export async function collectFees( const txParams = await ethereum.prepareGasOptions(undefined, CLMM_COLLECT_FEES_GAS_LIMIT); txParams.value = BigNumber.from(value.toString()); const tx = await positionManagerWithSigner.multicall([calldata], txParams); - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the fee amounts below were read before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); const token0FeeAmount = formatTokenAmount(feeAmount0.toString(), token0.decimals); const token1FeeAmount = formatTokenAmount(feeAmount1.toString(), token1.decimals); @@ -104,10 +108,10 @@ export async function collectFees( const quoteFeeAmountCollected = isBaseToken0 ? token1FeeAmount : token0FeeAmount; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseFeeAmountCollected, quoteFeeAmountCollected, }, diff --git a/src/connectors/pancakeswap/clmm-routes/createPool.ts b/src/connectors/pancakeswap/clmm-routes/createPool.ts index 78f43b8d8a..377ea3e775 100644 --- a/src/connectors/pancakeswap/clmm-routes/createPool.ts +++ b/src/connectors/pancakeswap/clmm-routes/createPool.ts @@ -7,6 +7,7 @@ import { FastifyPluginAsync } from 'fastify'; import JSBI from 'jsbi'; import { Ethereum, TokenInfo } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; @@ -17,7 +18,6 @@ import { getPancakeswapV3FactoryAddress, getPancakeswapV3NftManagerAddress, } from '../pancakeswap.contracts'; -import { formatTokenAmount } from '../pancakeswap.utils'; import { PancakeswapClmmCreatePoolRequest } from '../schemas'; // Pancakeswap V3 supported fee tiers (hundredths of a bip). 100=0.01%, 500=0.05%, 2500=0.25%, 10000=1.00%. @@ -184,30 +184,30 @@ export async function createPool( logger.info(`Creating Pancakeswap V3 pool via tx ${tx.hash}`); - const receipt = await ethereum.handleTransactionExecution(tx); + // A revert throws out of here (400 TRANSACTION_FAILED) — it is never reported as PENDING. + const outcome = await ethereum.handleTransactionConfirmation(tx); // Read the (now-created) pool address from the factory — the authoritative source. const poolAddress: string = await factory.getPool(token0.address, token1.address, fee); - if (receipt && receipt.status === 1) { - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); // ETH has 18 decimals + if (!outcome.confirmed) { + // Timed out but still broadcasting — report PENDING with the tx hash so the caller can reconcile it. return { - signature: receipt.transactionHash, - status: 1, // CONFIRMED + signature: outcome.signature, + status: TransactionStatus.PENDING, poolAddress, price: seedPrice, - data: { - fee: gasFee, - }, }; } - // Timed out (still broadcasting) or reverted — report as pending with the tx hash. return { - signature: receipt ? receipt.transactionHash : tx.hash, - status: 0, // PENDING + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, poolAddress, price: seedPrice, + data: { + fee: outcome.fee, + }, }; } diff --git a/src/connectors/pancakeswap/clmm-routes/executeSwap.ts b/src/connectors/pancakeswap/clmm-routes/executeSwap.ts index d14aae5085..3ae9c9e64b 100644 --- a/src/connectors/pancakeswap/clmm-routes/executeSwap.ts +++ b/src/connectors/pancakeswap/clmm-routes/executeSwap.ts @@ -2,8 +2,9 @@ import { encodeSqrtRatioX96 } from '@uniswap/v3-sdk'; import { BigNumber, Contract, utils } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; -import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { Ethereum, EthereumTransactionOutcome } from '../../../chains/ethereum/ethereum'; import { EthereumLedger } from '../../../chains/ethereum/ethereum-ledger'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { ExecuteSwapRequestType, SwapExecuteResponseType, SwapExecuteResponse } from '../../../schemas/router-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; @@ -109,7 +110,7 @@ export async function executeClmmSwap( ).toString(), }; - let receipt; + let outcome: EthereumTransactionOutcome; try { if (isHardwareWallet) { @@ -186,7 +187,7 @@ export async function executeClmmSwap( logger.info(`Transaction sent: ${txResponse.hash}`); // Wait for confirmation with timeout - receipt = await ethereum.handleTransactionExecution(txResponse); + outcome = await ethereum.handleTransactionConfirmation(txResponse); } else { // Regular wallet flow let wallet; @@ -249,19 +250,18 @@ export async function executeClmmSwap( logger.info(`Transaction sent: ${tx.hash}`); // Wait for transaction confirmation - receipt = await ethereum.handleTransactionExecution(tx); + outcome = await ethereum.handleTransactionConfirmation(tx); } - // Check if the transaction was successful - if (receipt.status === 0) { - logger.error(`Transaction failed on-chain. Receipt: ${JSON.stringify(receipt)}`); - throw httpErrors.internalServerError( - 'Transaction reverted on-chain. This could be due to slippage, insufficient funds, or other blockchain issues.', - ); + // A revert threw out of the confirmation helper as a 400 TRANSACTION_FAILED. What is left + // is a transaction that is still pending after the extended poll: report it as PENDING + // with its hash rather than dereferencing a null receipt and losing the hash to a 500. + if (!outcome.confirmed) { + return { signature: outcome.signature, status: TransactionStatus.PENDING }; } - logger.info(`Transaction hash: ${receipt.transactionHash}`); - logger.info(`Gas used: ${receipt.gasUsed.toString()}`); + logger.info(`Transaction hash: ${outcome.signature}`); + logger.info(`Gas used: ${outcome.receipt.gasUsed.toString()}`); // Calculate amounts using quote values const amountIn = quote.estimatedAmountIn; @@ -271,25 +271,19 @@ export async function executeClmmSwap( const baseTokenBalanceChange = side === 'BUY' ? amountOut : -amountIn; const quoteTokenBalanceChange = side === 'BUY' ? -amountIn : amountOut; - // Calculate gas fee (formatTokenAmount already returns a number) - const gasFee = formatTokenAmount( - receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), - 18, // ETH has 18 decimals - ); - // Determine token addresses for computed fields const tokenIn = quote.inputToken.address; const tokenOut = quote.outputToken.address; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { tokenIn, tokenOut, amountIn, amountOut, - fee: gasFee, + fee: outcome.fee, baseTokenBalanceChange, quoteTokenBalanceChange, slippagePct, diff --git a/src/connectors/pancakeswap/clmm-routes/openPosition.ts b/src/connectors/pancakeswap/clmm-routes/openPosition.ts index 2574bbe477..094587623c 100644 --- a/src/connectors/pancakeswap/clmm-routes/openPosition.ts +++ b/src/connectors/pancakeswap/clmm-routes/openPosition.ts @@ -6,6 +6,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Address } from 'viem'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { OpenPositionRequestType, OpenPositionRequest, @@ -239,11 +240,15 @@ export async function openPosition( } // Wait for transaction confirmation - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — there is no mint log to read yet, so no positionAddress and no amounts. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } // Find the NFT ID from the transaction logs let positionId = ''; - for (const log of receipt.logs) { + for (const log of outcome.receipt.logs) { if ( log.address.toLowerCase() === positionManagerAddress.toLowerCase() && log.topics[0] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' && @@ -254,8 +259,15 @@ export async function openPosition( } } - // Calculate gas fee - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); + if (!positionId) { + // The transaction confirmed but no NFT-mint Transfer log was found. Returning a CONFIRMED + // response with an empty positionAddress would have the caller record a position it can + // never address — fail loudly, naming the transaction so the position stays recoverable. + throw httpErrors.internalServerError( + `Position opened in transaction ${outcome.signature} but no position NFT mint was found in its logs. ` + + `Inspect the transaction to recover the position ID.`, + ); + } // For position rent, we're using the estimated gas cost since Ethereum doesn't have rent like Solana const positionRent = 0; @@ -269,10 +281,10 @@ export async function openPosition( const quoteAmountUsed = isBaseToken0 ? actualToken1Amount : actualToken0Amount; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, positionAddress: positionId, positionRent, baseTokenAmountAdded: baseAmountUsed, diff --git a/src/connectors/pancakeswap/clmm-routes/removeLiquidity.ts b/src/connectors/pancakeswap/clmm-routes/removeLiquidity.ts index 9487cb9f68..71417ff1e6 100644 --- a/src/connectors/pancakeswap/clmm-routes/removeLiquidity.ts +++ b/src/connectors/pancakeswap/clmm-routes/removeLiquidity.ts @@ -7,6 +7,7 @@ import JSBI from 'jsbi'; import { Address } from 'viem'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { RemoveLiquidityRequestType, RemoveLiquidityRequest, @@ -133,9 +134,12 @@ export async function removeLiquidity( const txParams = await ethereum.prepareGasOptions(undefined, CLMM_REMOVE_LIQUIDITY_GAS_LIMIT); txParams.value = BigNumber.from(value.toString()); const tx = await positionManagerWithSigner.multicall([calldata], txParams); - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the amounts below were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); const token0AmountRemoved = formatTokenAmount(totalAmount0.quotient.toString(), token0.decimals); const token1AmountRemoved = formatTokenAmount(totalAmount1.quotient.toString(), token1.decimals); @@ -143,10 +147,10 @@ export async function removeLiquidity( const quoteTokenAmountRemoved = isBaseToken0 ? token1AmountRemoved : token0AmountRemoved; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseTokenAmountRemoved, quoteTokenAmountRemoved, }, diff --git a/src/connectors/uniswap/amm-routes/addLiquidity.ts b/src/connectors/uniswap/amm-routes/addLiquidity.ts index f8dec168aa..639e722e40 100644 --- a/src/connectors/uniswap/amm-routes/addLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/addLiquidity.ts @@ -8,6 +8,7 @@ import { re } from 'mathjs'; import { Ethereum } from '../../../chains/ethereum/ethereum'; import { wrapEthereum } from '../../../chains/ethereum/routes/wrap'; import { AddLiquidityResponseType, AddLiquidityResponse } from '../../../schemas/amm-schema'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { UniswapAmmAddLiquidityRequest } from '../schemas'; @@ -257,19 +258,17 @@ async function addLiquidityInternal( } // Wait for transaction confirmation - const receipt = await ethereum.handleTransactionExecution(tx); - - // Calculate gas fee - const gasFee = formatTokenAmount( - receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), - 18, // ETH has 18 decimals - ); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the quoted amounts were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseTokenAmountAdded: quote.baseTokenAmount, quoteTokenAmountAdded: quote.quoteTokenAmount, ...(baseWrapTxHash && { baseWrapTxHash }), diff --git a/src/connectors/uniswap/amm-routes/createPool.ts b/src/connectors/uniswap/amm-routes/createPool.ts index c37b4a5408..8cf250598c 100644 --- a/src/connectors/uniswap/amm-routes/createPool.ts +++ b/src/connectors/uniswap/amm-routes/createPool.ts @@ -7,6 +7,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Ethereum, TokenInfo } from '../../../chains/ethereum/ethereum'; import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/amm-schema'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { UniswapAmmCreatePoolRequest } from '../schemas'; @@ -259,32 +260,33 @@ export async function createPool( logger.info(`Creating Uniswap V2 pool ${baseTokenInfo.symbol}/${quoteTokenInfo.symbol} via tx ${tx.hash}`); - const receipt = await ethereum.handleTransactionExecution(tx); + // A revert throws out of here (400 TRANSACTION_FAILED) — it is never reported as PENDING. + const outcome = await ethereum.handleTransactionConfirmation(tx); // Read the (now-created) pair address from the factory — authoritative source of the pool address. const pairAddress: string = await factory.getPair(baseTokenInfo.address, quoteTokenInfo.address); - if (receipt && receipt.status === 1) { - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); // ETH has 18 decimals + if (!outcome.confirmed) { + // Timed out but still broadcasting — report PENDING with the tx hash so the caller can + // reconcile it. The seed amounts below have not moved, so they are deliberately omitted. return { - signature: receipt.transactionHash, - status: 1, // CONFIRMED + signature: outcome.signature, + status: TransactionStatus.PENDING, poolAddress: pairAddress, price: seedPrice, - data: { - fee: gasFee, - baseTokenAmountAdded: baseTokenAmount, - quoteTokenAmountAdded: effectiveQuoteAmount, - }, }; } - // Timed out (still broadcasting) or reverted — report as pending with the tx hash. return { - signature: receipt ? receipt.transactionHash : tx.hash, - status: 0, // PENDING + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, poolAddress: pairAddress, price: seedPrice, + data: { + fee: outcome.fee, + baseTokenAmountAdded: baseTokenAmount, + quoteTokenAmountAdded: effectiveQuoteAmount, + }, }; } diff --git a/src/connectors/uniswap/amm-routes/executeSwap.ts b/src/connectors/uniswap/amm-routes/executeSwap.ts index b67af4d88b..5c0e150593 100644 --- a/src/connectors/uniswap/amm-routes/executeSwap.ts +++ b/src/connectors/uniswap/amm-routes/executeSwap.ts @@ -1,9 +1,10 @@ import { BigNumber, Contract, utils } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; -import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { Ethereum, EthereumTransactionOutcome } from '../../../chains/ethereum/ethereum'; import { EthereumLedger } from '../../../chains/ethereum/ethereum-ledger'; import { getEthereumChainConfig } from '../../../chains/ethereum/ethereum.config'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { ExecuteSwapRequestType, SwapExecuteResponseType, SwapExecuteResponse } from '../../../schemas/router-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; @@ -86,7 +87,7 @@ export async function executeAmmSwap( // Prepare transaction parameters const deadline = Math.floor(Date.now() / 1000) + 60 * 20; // 20 minutes from now - let receipt; + let outcome: EthereumTransactionOutcome; try { if (isHardwareWallet) { @@ -151,7 +152,7 @@ export async function executeAmmSwap( logger.info(`Transaction sent: ${txResponse.hash}`); // Wait for confirmation with timeout - receipt = await ethereum.handleTransactionExecution(txResponse); + outcome = await ethereum.handleTransactionConfirmation(txResponse); } else { // Regular wallet flow let wallet; @@ -208,19 +209,18 @@ export async function executeAmmSwap( logger.info(`Transaction sent: ${tx.hash}`); // Wait for transaction confirmation - receipt = await ethereum.handleTransactionExecution(tx); + outcome = await ethereum.handleTransactionConfirmation(tx); } - // Check if the transaction was successful - if (receipt.status === 0) { - logger.error(`Transaction failed on-chain. Receipt: ${JSON.stringify(receipt)}`); - throw httpErrors.internalServerError( - 'Transaction reverted on-chain. This could be due to slippage, insufficient funds, or other blockchain issues.', - ); + // A revert threw out of the confirmation helper as a 400 TRANSACTION_FAILED. What is left + // is a transaction that is still pending after the extended poll: report it as PENDING + // with its hash rather than dereferencing a null receipt and losing the hash to a 500. + if (!outcome.confirmed) { + return { signature: outcome.signature, status: TransactionStatus.PENDING }; } - logger.info(`Transaction confirmed: ${receipt.transactionHash}`); - logger.info(`Gas used: ${receipt.gasUsed.toString()}`); + logger.info(`Transaction confirmed: ${outcome.signature}`); + logger.info(`Gas used: ${outcome.receipt.gasUsed.toString()}`); // Calculate amounts using quote values const amountIn = quote.estimatedAmountIn; @@ -230,25 +230,19 @@ export async function executeAmmSwap( const baseTokenBalanceChange = side === 'BUY' ? amountOut : -amountIn; const quoteTokenBalanceChange = side === 'BUY' ? -amountIn : amountOut; - // Calculate gas fee (formatTokenAmount already returns a number) - const gasFee = formatTokenAmount( - receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), - 18, // ETH has 18 decimals - ); - // Determine token addresses for computed fields const tokenIn = quote.inputToken.address; const tokenOut = quote.outputToken.address; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { tokenIn, tokenOut, amountIn, amountOut, - fee: gasFee, + fee: outcome.fee, baseTokenBalanceChange, quoteTokenBalanceChange, slippagePct, diff --git a/src/connectors/uniswap/amm-routes/removeLiquidity.ts b/src/connectors/uniswap/amm-routes/removeLiquidity.ts index 25b0239c1c..b5927a044e 100644 --- a/src/connectors/uniswap/amm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/amm-routes/removeLiquidity.ts @@ -6,6 +6,7 @@ import { FastifyPluginAsync } from 'fastify'; import { Ethereum } from '../../../chains/ethereum/ethereum'; import { RemoveLiquidityResponseType, RemoveLiquidityResponse } from '../../../schemas/amm-schema'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; import { UniswapAmmRemoveLiquidityRequest } from '../schemas'; @@ -119,16 +120,20 @@ export async function removeLiquidity( ); } - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the expected amounts were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } + const baseTokenAmountRemoved = formatTokenAmount(expectedBaseTokenAmount.toString(), baseTokenObj.decimals); const quoteTokenAmountRemoved = formatTokenAmount(expectedQuoteTokenAmount.toString(), quoteTokenObj.decimals); - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseTokenAmountRemoved, quoteTokenAmountRemoved, }, diff --git a/src/connectors/uniswap/clmm-routes/addLiquidity.ts b/src/connectors/uniswap/clmm-routes/addLiquidity.ts index 9594bbcd46..2dea1ad872 100644 --- a/src/connectors/uniswap/clmm-routes/addLiquidity.ts +++ b/src/connectors/uniswap/clmm-routes/addLiquidity.ts @@ -7,6 +7,7 @@ import { FastifyPluginAsync } from 'fastify'; import JSBI from 'jsbi'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { AddLiquidityResponseType, AddLiquidityResponse } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; @@ -152,9 +153,12 @@ export async function addLiquidity( const txParams = await ethereum.prepareGasOptions(undefined, CLMM_ADD_LIQUIDITY_GAS_LIMIT); txParams.value = BigNumber.from(value.toString()); const tx = await positionManagerWithSigner.multicall([calldata], txParams); - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the mint amounts below were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); const actualToken0Amount = formatTokenAmount(newPosition.mintAmounts.amount0.toString(), token0.decimals); const actualToken1Amount = formatTokenAmount(newPosition.mintAmounts.amount1.toString(), token1.decimals); @@ -162,10 +166,10 @@ export async function addLiquidity( const actualQuoteAmount = isBaseToken0 ? actualToken1Amount : actualToken0Amount; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseTokenAmountAdded: actualBaseAmount, quoteTokenAmountAdded: actualQuoteAmount, }, diff --git a/src/connectors/uniswap/clmm-routes/closePosition.ts b/src/connectors/uniswap/clmm-routes/closePosition.ts index 972dabf7f1..b9ea4feacd 100644 --- a/src/connectors/uniswap/clmm-routes/closePosition.ts +++ b/src/connectors/uniswap/clmm-routes/closePosition.ts @@ -6,6 +6,7 @@ import { FastifyPluginAsync } from 'fastify'; import JSBI from 'jsbi'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { ClosePositionRequestType, ClosePositionRequest, @@ -153,10 +154,11 @@ export async function closePosition( const tx = await positionManagerWithSigner.multicall([calldata], txParams); // Wait for transaction confirmation - const receipt = await ethereum.handleTransactionExecution(tx); - - // Calculate gas fee - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the amounts below were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } // Calculate token amounts removed including fees const token0AmountRemoved = formatTokenAmount(totalAmount0.quotient.toString(), token0.decimals); @@ -177,10 +179,10 @@ export async function closePosition( const positionRentRefunded = 0; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, positionRentRefunded, baseTokenAmountRemoved, quoteTokenAmountRemoved, diff --git a/src/connectors/uniswap/clmm-routes/collectFees.ts b/src/connectors/uniswap/clmm-routes/collectFees.ts index 718d9bb8e8..58918b30ad 100644 --- a/src/connectors/uniswap/clmm-routes/collectFees.ts +++ b/src/connectors/uniswap/clmm-routes/collectFees.ts @@ -5,6 +5,7 @@ import { BigNumber } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { CollectFeesRequestType, CollectFeesRequest, @@ -114,10 +115,11 @@ export async function collectFees( const tx = await positionManagerWithSigner.multicall([calldata], txParams); // Wait for transaction confirmation - const receipt = await ethereum.handleTransactionExecution(tx); - - // Calculate gas fee - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the fee amounts below were read before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } // Calculate fee amounts collected const token0FeeAmount = formatTokenAmount(feeAmount0.toString(), token0.decimals); @@ -128,10 +130,10 @@ export async function collectFees( const quoteFeeAmountCollected = isBaseToken0 ? token1FeeAmount : token0FeeAmount; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseFeeAmountCollected, quoteFeeAmountCollected, }, diff --git a/src/connectors/uniswap/clmm-routes/createPool.ts b/src/connectors/uniswap/clmm-routes/createPool.ts index 2bc7ae11d7..5c11303419 100644 --- a/src/connectors/uniswap/clmm-routes/createPool.ts +++ b/src/connectors/uniswap/clmm-routes/createPool.ts @@ -7,6 +7,7 @@ import { FastifyPluginAsync } from 'fastify'; import JSBI from 'jsbi'; import { Ethereum, TokenInfo } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { CreatePoolResponse, CreatePoolResponseType } from '../../../schemas/clmm-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; @@ -18,7 +19,6 @@ import { getUniswapV3FactoryAddress, getUniswapV3NftManagerAddress, } from '../uniswap.contracts'; -import { formatTokenAmount } from '../uniswap.utils'; // Uniswap V3 supported fee tiers (hundredths of a bip). 100=0.01%, 500=0.05%, 3000=0.30%, 10000=1.00%. const VALID_FEE_TIERS = [100, 500, 3000, 10000]; @@ -186,30 +186,30 @@ export async function createPool( logger.info(`Creating Uniswap V3 pool via tx ${tx.hash}`); - const receipt = await ethereum.handleTransactionExecution(tx); + // A revert throws out of here (400 TRANSACTION_FAILED) — it is never reported as PENDING. + const outcome = await ethereum.handleTransactionConfirmation(tx); // Read the (now-created) pool address from the factory — the authoritative source. const poolAddress: string = await factory.getPool(token0.address, token1.address, fee); - if (receipt && receipt.status === 1) { - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); // ETH has 18 decimals + if (!outcome.confirmed) { + // Timed out but still broadcasting — report PENDING with the tx hash so the caller can reconcile it. return { - signature: receipt.transactionHash, - status: 1, // CONFIRMED + signature: outcome.signature, + status: TransactionStatus.PENDING, poolAddress, price: seedPrice, - data: { - fee: gasFee, - }, }; } - // Timed out (still broadcasting) or reverted — report as pending with the tx hash. return { - signature: receipt ? receipt.transactionHash : tx.hash, - status: 0, // PENDING + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, poolAddress, price: seedPrice, + data: { + fee: outcome.fee, + }, }; } diff --git a/src/connectors/uniswap/clmm-routes/executeSwap.ts b/src/connectors/uniswap/clmm-routes/executeSwap.ts index 5c3c13eaa4..33b404a69c 100644 --- a/src/connectors/uniswap/clmm-routes/executeSwap.ts +++ b/src/connectors/uniswap/clmm-routes/executeSwap.ts @@ -1,8 +1,9 @@ import { BigNumber, Contract, utils } from 'ethers'; import { FastifyPluginAsync } from 'fastify'; -import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { Ethereum, EthereumTransactionOutcome } from '../../../chains/ethereum/ethereum'; import { EthereumLedger } from '../../../chains/ethereum/ethereum-ledger'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { ExecuteSwapRequestType, SwapExecuteResponseType, SwapExecuteResponse } from '../../../schemas/router-schema'; import { httpErrors } from '../../../services/error-handler'; import { logger } from '../../../services/logger'; @@ -103,7 +104,7 @@ export async function executeClmmSwap( sqrtPriceLimitX96: '0', }; - let receipt; + let outcome: EthereumTransactionOutcome; try { if (isHardwareWallet) { @@ -180,7 +181,7 @@ export async function executeClmmSwap( logger.info(`Transaction sent: ${txResponse.hash}`); // Wait for confirmation with timeout - receipt = await ethereum.handleTransactionExecution(txResponse); + outcome = await ethereum.handleTransactionConfirmation(txResponse); } else { // Regular wallet flow let wallet; @@ -242,19 +243,18 @@ export async function executeClmmSwap( logger.info(`Transaction sent: ${tx.hash}`); // Wait for transaction confirmation - receipt = await ethereum.handleTransactionExecution(tx); + outcome = await ethereum.handleTransactionConfirmation(tx); } - // Check if the transaction was successful - if (receipt.status === 0) { - logger.error(`Transaction failed on-chain. Receipt: ${JSON.stringify(receipt)}`); - throw httpErrors.internalServerError( - 'Transaction reverted on-chain. This could be due to slippage, insufficient funds, or other blockchain issues.', - ); + // A revert threw out of the confirmation helper as a 400 TRANSACTION_FAILED. What is left + // is a transaction that is still pending after the extended poll: report it as PENDING + // with its hash rather than dereferencing a null receipt and losing the hash to a 500. + if (!outcome.confirmed) { + return { signature: outcome.signature, status: TransactionStatus.PENDING }; } - logger.info(`Transaction confirmed: ${receipt.transactionHash}`); - logger.info(`Gas used: ${receipt.gasUsed.toString()}`); + logger.info(`Transaction confirmed: ${outcome.signature}`); + logger.info(`Gas used: ${outcome.receipt.gasUsed.toString()}`); // Calculate amounts using quote values const amountIn = quote.estimatedAmountIn; @@ -264,25 +264,19 @@ export async function executeClmmSwap( const baseTokenBalanceChange = side === 'BUY' ? amountOut : -amountIn; const quoteTokenBalanceChange = side === 'BUY' ? -amountIn : amountOut; - // Calculate gas fee (formatTokenAmount already returns a number) - const gasFee = formatTokenAmount( - receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), - 18, // ETH has 18 decimals - ); - // Determine token addresses for computed fields const tokenIn = quote.inputToken.address; const tokenOut = quote.outputToken.address; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { tokenIn, tokenOut, amountIn, amountOut, - fee: gasFee, + fee: outcome.fee, baseTokenBalanceChange, quoteTokenBalanceChange, slippagePct, diff --git a/src/connectors/uniswap/clmm-routes/openPosition.ts b/src/connectors/uniswap/clmm-routes/openPosition.ts index 108987b81f..d98166b4ce 100644 --- a/src/connectors/uniswap/clmm-routes/openPosition.ts +++ b/src/connectors/uniswap/clmm-routes/openPosition.ts @@ -9,6 +9,7 @@ import JSBI from 'jsbi'; const CLMM_OPEN_POSITION_GAS_LIMIT = 600000; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { OpenPositionRequestType, OpenPositionRequest, @@ -235,11 +236,15 @@ export async function openPosition( } // Wait for transaction confirmation - const receipt = await ethereum.handleTransactionExecution(tx); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — there is no mint log to read yet, so no positionAddress and no amounts. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } // Find the NFT ID from the transaction logs let positionId = ''; - for (const log of receipt.logs) { + for (const log of outcome.receipt.logs) { if ( log.address.toLowerCase() === positionManagerAddress.toLowerCase() && log.topics[0] === '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' && @@ -250,8 +255,15 @@ export async function openPosition( } } - // Calculate gas fee - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); + if (!positionId) { + // The transaction confirmed but no NFT-mint Transfer log was found. Returning a CONFIRMED + // response with an empty positionAddress would have the caller record a position it can + // never address — fail loudly, naming the transaction so the position stays recoverable. + throw httpErrors.internalServerError( + `Position opened in transaction ${outcome.signature} but no position NFT mint was found in its logs. ` + + `Inspect the transaction to recover the position ID.`, + ); + } // For position rent, we're using the estimated gas cost since Ethereum doesn't have rent like Solana const positionRent = 0; @@ -265,10 +277,10 @@ export async function openPosition( const quoteAmountUsed = isBaseToken0 ? actualToken1Amount : actualToken0Amount; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, positionAddress: positionId, positionRent, baseTokenAmountAdded: baseAmountUsed, @@ -364,8 +376,9 @@ export const openPositionRoute: FastifyPluginAsync = async (fastify) => { throw httpErrors.badRequest('Insufficient funds to complete the transaction'); } - // Generic error - throw httpErrors.internalServerError('Failed to open position'); + // Generic error — keep the underlying message. Dropping it hid every real cause + // (including a lost transaction hash) behind an unactionable 'Failed to open position'. + throw httpErrors.internalServerError(`Failed to open position: ${e.message ?? String(e)}`); } }, ); diff --git a/src/connectors/uniswap/clmm-routes/removeLiquidity.ts b/src/connectors/uniswap/clmm-routes/removeLiquidity.ts index 0198ea2d42..80313396e5 100644 --- a/src/connectors/uniswap/clmm-routes/removeLiquidity.ts +++ b/src/connectors/uniswap/clmm-routes/removeLiquidity.ts @@ -6,6 +6,7 @@ import { FastifyPluginAsync } from 'fastify'; import JSBI from 'jsbi'; import { Ethereum } from '../../../chains/ethereum/ethereum'; +import { TransactionStatus } from '../../../schemas/chain-schema'; import { RemoveLiquidityRequestType, RemoveLiquidityRequest, @@ -164,10 +165,11 @@ export async function removeLiquidity( const tx = await positionManagerWithSigner.multicall([calldata], txParams); // Wait for transaction confirmation - const receipt = await ethereum.handleTransactionExecution(tx); - - // Calculate gas fee - const gasFee = formatTokenAmount(receipt.gasUsed.mul(receipt.effectiveGasPrice).toString(), 18); + const outcome = await ethereum.handleTransactionConfirmation(tx); + if (!outcome.confirmed) { + // Still pending — the amounts below were computed before sending and have not moved. + return { signature: outcome.signature, status: TransactionStatus.PENDING }; + } // Calculate token amounts removed including fees const token0AmountRemoved = formatTokenAmount(totalAmount0.quotient.toString(), token0.decimals); @@ -178,10 +180,10 @@ export async function removeLiquidity( const quoteTokenAmountRemoved = isBaseToken0 ? token1AmountRemoved : token0AmountRemoved; return { - signature: receipt.transactionHash, - status: receipt.status, + signature: outcome.signature, + status: TransactionStatus.CONFIRMED, data: { - fee: gasFee, + fee: outcome.fee, baseTokenAmountRemoved, quoteTokenAmountRemoved, }, diff --git a/test/chains/ethereum/transaction-confirmation.test.ts b/test/chains/ethereum/transaction-confirmation.test.ts new file mode 100644 index 0000000000..ba5df56a67 --- /dev/null +++ b/test/chains/ethereum/transaction-confirmation.test.ts @@ -0,0 +1,83 @@ +import { BigNumber } from 'ethers'; + +import { Ethereum } from '../../../src/chains/ethereum/ethereum'; + +// Unit tests for Ethereum.prototype.handleTransactionConfirmation — the single +// confirmation gate every EVM route that sends a transaction goes through. +// +// The contract it has to hold: +// - no receipt (still pending) -> { confirmed: false } carrying the tx hash, never a throw, +// so a caller can reconcile a transaction that lands after the poll window closes. +// - reverted receipt (status 0) -> throws 400 TRANSACTION_FAILED. `receipt.status === 0` +// must never reach a response body, where 0 reads as TransactionStatus.PENDING. +// - confirmed receipt (status 1) -> { confirmed: true } with the receipt and the gas fee +// already converted to native units. +// +// The class constructor is private and pulls in network state, so the method is invoked +// through Ethereum.prototype..call() against a stub carrying only what it reads. + +type Stub = { handleTransactionExecution: jest.Mock }; + +const callHelper = (stub: Stub, tx: any) => (Ethereum.prototype as any).handleTransactionConfirmation.call(stub, tx); + +describe('Ethereum.handleTransactionConfirmation', () => { + it('reports a still-pending transaction as pending and preserves the tx hash', async () => { + const stub: Stub = { handleTransactionExecution: jest.fn().mockResolvedValue(null) }; + + const outcome = await callHelper(stub, { hash: '0xpendinghash' }); + + expect(outcome).toEqual({ confirmed: false, signature: '0xpendinghash' }); + }); + + it('throws 400 TRANSACTION_FAILED for a reverted transaction instead of reporting PENDING', async () => { + const stub: Stub = { + handleTransactionExecution: jest.fn().mockResolvedValue({ + status: 0, + transactionHash: '0xrevertedhash', + gasUsed: BigNumber.from(21_000), + effectiveGasPrice: BigNumber.from('1000000000'), + }), + }; + + const error: any = await callHelper(stub, { hash: '0xrevertedhash' }).catch((e: any) => e); + + expect(error.statusCode).toBe(400); + expect(error.code).toBe('TRANSACTION_FAILED'); + expect(error.message).toContain('0xrevertedhash'); + expect(error.message).toMatch(/reverted on-chain/); + }); + + it('returns the receipt and the gas fee in native units for a confirmed transaction', async () => { + const receipt = { + status: 1, + transactionHash: '0xconfirmedhash', + logs: [], + // 21,000 gas at 1 gwei = 0.000021 ETH + gasUsed: BigNumber.from(21_000), + effectiveGasPrice: BigNumber.from('1000000000'), + }; + const stub: Stub = { handleTransactionExecution: jest.fn().mockResolvedValue(receipt) }; + + const outcome = await callHelper(stub, { hash: '0xconfirmedhash' }); + + expect(outcome).toEqual({ + confirmed: true, + signature: '0xconfirmedhash', + receipt, + fee: 0.000021, + }); + }); + + it('treats a receipt with no status as pending rather than guessing', async () => { + const stub: Stub = { + handleTransactionExecution: jest.fn().mockResolvedValue({ + status: undefined, + transactionHash: '0xnostatushash', + }), + }; + + const outcome = await callHelper(stub, { hash: '0xnostatushash' }); + + expect(outcome).toEqual({ confirmed: false, signature: '0xnostatushash' }); + }); +}); diff --git a/test/connectors/uniswap/amm-routes/remove-liquidity-confirmation.test.ts b/test/connectors/uniswap/amm-routes/remove-liquidity-confirmation.test.ts new file mode 100644 index 0000000000..3872efa394 --- /dev/null +++ b/test/connectors/uniswap/amm-routes/remove-liquidity-confirmation.test.ts @@ -0,0 +1,140 @@ +import { BigNumber } from 'ethers'; + +import { Ethereum } from '../../../../src/chains/ethereum/ethereum'; +import { Uniswap } from '../../../../src/connectors/uniswap/uniswap'; +import { getUniswapPoolInfo } from '../../../../src/connectors/uniswap/uniswap.utils'; +import { fastifyWithTypeProvider } from '../../../utils/testUtils'; + +jest.mock('../../../../src/chains/ethereum/ethereum'); +jest.mock('../../../../src/connectors/uniswap/uniswap'); +jest.mock('@ethersproject/contracts'); +jest.mock('../../../../src/connectors/uniswap/amm-routes/positionInfo', () => ({ + ...jest.requireActual('../../../../src/connectors/uniswap/amm-routes/positionInfo'), + checkLPAllowance: jest.fn().mockResolvedValue(undefined), +})); +jest.mock('../../../../src/connectors/uniswap/uniswap.utils', () => ({ + ...jest.requireActual('../../../../src/connectors/uniswap/uniswap.utils'), + getUniswapPoolInfo: jest.fn(), +})); + +// The AMM side of the same confirmation contract (the CLMM side is pinned in +// clmm-routes/collect-fees-confirmation.test.ts). remove-liquidity is the route where the old +// behaviour was worst: `expectedBaseTokenAmount`/`expectedQuoteTokenAmount` are derived from +// reserves read BEFORE sending, so a reverted transaction used to report those withdrawals +// alongside status 0 — read downstream as "still pending" forever. + +const USDC = { address: '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', symbol: 'USDC', decimals: 6 }; +const DAI = { address: '0x50c5725949a6f0c72e6c4a641f24049a917db0cb', symbol: 'DAI', decimals: 18 }; +const mockWallet = '0x0000000000000000000000000000000000000001'; +const poolAddress = '0xd0b53d9277642d899df5c87a3966a349a798f224'; +const txHash = '0x3333333333333333333333333333333333333333333333333333333333333333'; + +const { Ethereum: RealEthereum } = jest.requireActual('../../../../src/chains/ethereum/ethereum'); + +const buildApp = async () => { + const server = fastifyWithTypeProvider(); + await server.register(require('@fastify/sensible')); + const { removeLiquidityRoute } = await import('../../../../src/connectors/uniswap/amm-routes/removeLiquidity'); + await server.register(removeLiquidityRoute); + return server; +}; + +const primeMocks = (receipt: any) => { + (getUniswapPoolInfo as jest.Mock).mockResolvedValue({ + baseTokenAddress: USDC.address, + quoteTokenAddress: DAI.address, + }); + + (Uniswap.getInstance as jest.Mock).mockResolvedValue({ + getToken: jest.fn((address: string) => Promise.resolve(address === USDC.address ? USDC : DAI)), + }); + + const ethereum: any = { + provider: {}, + getWallet: jest.fn().mockResolvedValue({ address: mockWallet }), + prepareGasOptions: jest.fn().mockResolvedValue({}), + handleTransactionExecution: jest.fn().mockResolvedValue(receipt), + }; + ethereum.handleTransactionConfirmation = RealEthereum.prototype.handleTransactionConfirmation.bind(ethereum); + (Ethereum.getInstance as jest.Mock).mockResolvedValue(ethereum); + + const { Contract } = require('@ethersproject/contracts'); + (Contract as jest.Mock).mockImplementation(() => ({ + // LP pair reads: the wallet holds 10% of a pool with 100 USDC / 100 DAI of reserves, + // so removing 50% is worth 5 USDC + 5 DAI. + balanceOf: jest.fn().mockResolvedValue(BigNumber.from('1000000000000000000')), + token0: jest.fn().mockResolvedValue(USDC.address), + token1: jest.fn().mockResolvedValue(DAI.address), + totalSupply: jest.fn().mockResolvedValue(BigNumber.from('10000000000000000000')), + getReserves: jest.fn().mockResolvedValue([BigNumber.from('100000000'), BigNumber.from('100000000000000000000')]), + // Router write + removeLiquidity: jest.fn().mockResolvedValue({ hash: txHash }), + })); +}; + +const remove = (server: any) => + server.inject({ + method: 'POST', + url: '/remove-liquidity', + payload: { network: 'base', walletAddress: mockWallet, poolAddress, percentageToRemove: 50 }, + }); + +describe('POST /remove-liquidity (Uniswap V2 AMM) — transaction confirmation', () => { + let server: any; + + beforeEach(async () => { + jest.clearAllMocks(); + (Ethereum.getWalletAddressExample as jest.Mock) = jest.fn().mockResolvedValue(mockWallet); + server = await buildApp(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('returns the pending shape with the tx hash and no withdrawn amounts', async () => { + primeMocks(null); + + const response = await remove(server); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + expect(body.signature).toBe(txHash); + expect(body.status).toBe(0); // TransactionStatus.PENDING + expect(body.data).toBeUndefined(); + }); + + it('fails loudly (400 TRANSACTION_FAILED) on a revert rather than booking amounts as pending', async () => { + primeMocks({ + status: 0, + transactionHash: txHash, + gasUsed: BigNumber.from(21_000), + effectiveGasPrice: BigNumber.from('1000000000'), + }); + + const response = await remove(server); + + expect(response.statusCode).toBe(400); + expect(JSON.parse(response.body).message).toContain(txHash); + expect(response.body).not.toContain('baseTokenAmountRemoved'); + }); + + it('returns the confirmed shape with the withdrawn amounts and the receipt gas fee', async () => { + primeMocks({ + status: 1, + transactionHash: txHash, + logs: [], + gasUsed: BigNumber.from(21_000), + effectiveGasPrice: BigNumber.from('1000000000'), + }); + + const response = await remove(server); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + expect(body.status).toBe(1); // TransactionStatus.CONFIRMED + expect(body.data.baseTokenAmountRemoved).toBe(5); + expect(body.data.quoteTokenAmountRemoved).toBe(5); + expect(body.data.fee).toBe(0.000021); + }); +}); diff --git a/test/connectors/uniswap/clmm-routes/collect-fees-confirmation.test.ts b/test/connectors/uniswap/clmm-routes/collect-fees-confirmation.test.ts new file mode 100644 index 0000000000..86ecd4c0e4 --- /dev/null +++ b/test/connectors/uniswap/clmm-routes/collect-fees-confirmation.test.ts @@ -0,0 +1,143 @@ +import { Token } from '@uniswap/sdk-core'; +import { BigNumber } from 'ethers'; + +import { Ethereum } from '../../../../src/chains/ethereum/ethereum'; +import { Uniswap } from '../../../../src/connectors/uniswap/uniswap'; +import { fastifyWithTypeProvider } from '../../../utils/testUtils'; + +jest.mock('../../../../src/chains/ethereum/ethereum'); +jest.mock('../../../../src/connectors/uniswap/uniswap'); +jest.mock('@ethersproject/contracts'); + +// Route-level contract for an EVM CLMM liquidity route once the transaction has been sent. +// collect-fees stands in for every route on the shared confirmation gate: the amounts in its +// `data` are read BEFORE sending, so reporting them for anything but a confirmed transaction +// books tokens that never moved. +// +// Pinned here: +// - still pending -> 200 { signature, status: 0 } and NO data (used to be a TypeError on a +// null receipt, turned into a 500 that threw the tx hash away). +// - reverted -> 400 TRANSACTION_FAILED with no amounts (used to be status 0 == PENDING +// alongside the pre-send fee amounts, which a poller waits on forever). +// - confirmed -> 200 { status: 1, data } with the gas fee from the receipt. + +const WETH = new Token(8453, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'); +const USDC = new Token(8453, '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', 6, 'USDC', 'USD Coin'); +const mockWallet = '0x0000000000000000000000000000000000000001'; +const positionAddress = '1234'; +const txHash = '0x1111111111111111111111111111111111111111111111111111111111111111'; + +// The real confirmation helper, bound to the stubbed instance, so these tests exercise the +// gate itself rather than a mock of it. +const { Ethereum: RealEthereum } = jest.requireActual('../../../../src/chains/ethereum/ethereum'); + +const buildApp = async () => { + const server = fastifyWithTypeProvider(); + await server.register(require('@fastify/sensible')); + const { collectFeesRoute } = await import('../../../../src/connectors/uniswap/clmm-routes/collectFees'); + await server.register(collectFeesRoute); + return server; +}; + +/** Wire the Uniswap/Ethereum/Contract stubs so the route reaches the confirmation gate. */ +const primeMocks = (receipt: any) => { + (Uniswap.getInstance as jest.Mock).mockResolvedValue({ + checkNFTOwnership: jest.fn().mockResolvedValue(undefined), + getToken: jest.fn((address: string) => + Promise.resolve(address.toLowerCase() === WETH.address.toLowerCase() ? WETH : USDC), + ), + }); + + const ethereum: any = { + provider: {}, + getWallet: jest.fn().mockResolvedValue({ address: mockWallet }), + prepareGasOptions: jest.fn().mockResolvedValue({}), + handleTransactionExecution: jest.fn().mockResolvedValue(receipt), + }; + ethereum.handleTransactionConfirmation = RealEthereum.prototype.handleTransactionConfirmation.bind(ethereum); + (Ethereum.getInstance as jest.Mock).mockResolvedValue(ethereum); + (Ethereum.getWalletAddressExample as jest.Mock).mockResolvedValue(mockWallet); + + const { Contract } = require('@ethersproject/contracts'); + (Contract as jest.Mock).mockImplementation(() => ({ + positions: jest.fn().mockResolvedValue({ + token0: WETH.address, + token1: USDC.address, + tokensOwed0: BigNumber.from('1000000000000000'), // 0.001 WETH + tokensOwed1: BigNumber.from('2000000'), // 2 USDC + }), + multicall: jest.fn().mockResolvedValue({ hash: txHash }), + })); +}; + +const collect = (server: any) => + server.inject({ + method: 'POST', + url: '/collect-fees', + payload: { network: 'base', walletAddress: mockWallet, positionAddress }, + }); + +describe('POST /collect-fees (Uniswap V3 CLMM) — transaction confirmation', () => { + let server: any; + + beforeEach(async () => { + jest.clearAllMocks(); + (Ethereum.getWalletAddressExample as jest.Mock) = jest.fn().mockResolvedValue(mockWallet); + server = await buildApp(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('returns the pending shape with the tx hash when the receipt is still missing', async () => { + primeMocks(null); + + const response = await collect(server); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + expect(body.signature).toBe(txHash); + expect(body.status).toBe(0); // TransactionStatus.PENDING + // No fabricated amounts: nothing has been collected yet. + expect(body.data).toBeUndefined(); + }); + + it('fails loudly (400 TRANSACTION_FAILED) on a revert instead of reporting PENDING with amounts', async () => { + primeMocks({ + status: 0, + transactionHash: txHash, + gasUsed: BigNumber.from(21_000), + effectiveGasPrice: BigNumber.from('1000000000'), + }); + + const response = await collect(server); + + expect(response.statusCode).toBe(400); + const body = JSON.parse(response.body); + expect(body.message).toContain(txHash); + expect(body.message).toMatch(/reverted on-chain/); + // The pre-send fee amounts must not appear anywhere in the response. + expect(response.body).not.toContain('baseFeeAmountCollected'); + }); + + it('returns the confirmed shape with the receipt gas fee', async () => { + primeMocks({ + status: 1, + transactionHash: txHash, + logs: [], + gasUsed: BigNumber.from(21_000), + effectiveGasPrice: BigNumber.from('1000000000'), + }); + + const response = await collect(server); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + expect(body.signature).toBe(txHash); + expect(body.status).toBe(1); // TransactionStatus.CONFIRMED + expect(body.data.fee).toBe(0.000021); + expect(body.data.baseFeeAmountCollected).toBe(0.001); + expect(body.data.quoteFeeAmountCollected).toBe(2); + }); +}); diff --git a/test/connectors/uniswap/clmm-routes/open-position-confirmation.test.ts b/test/connectors/uniswap/clmm-routes/open-position-confirmation.test.ts new file mode 100644 index 0000000000..ccea2033b3 --- /dev/null +++ b/test/connectors/uniswap/clmm-routes/open-position-confirmation.test.ts @@ -0,0 +1,171 @@ +import { Token } from '@uniswap/sdk-core'; +import { Pool, TickMath, encodeSqrtRatioX96 } from '@uniswap/v3-sdk'; +import { BigNumber } from 'ethers'; + +import { Ethereum } from '../../../../src/chains/ethereum/ethereum'; +import { Uniswap } from '../../../../src/connectors/uniswap/uniswap'; +import { getUniswapV3NftManagerAddress } from '../../../../src/connectors/uniswap/uniswap.contracts'; +import { getUniswapPoolInfo } from '../../../../src/connectors/uniswap/uniswap.utils'; +import { fastifyWithTypeProvider } from '../../../utils/testUtils'; + +jest.mock('../../../../src/chains/ethereum/ethereum'); +jest.mock('../../../../src/connectors/uniswap/uniswap'); +jest.mock('@ethersproject/contracts'); +jest.mock('../../../../src/connectors/uniswap/uniswap.utils', () => ({ + ...jest.requireActual('../../../../src/connectors/uniswap/uniswap.utils'), + getUniswapPoolInfo: jest.fn(), +})); + +// open-position is the route where a confirmed transaction can still fail to produce a usable +// result: the position's address only exists in the NFT-mint Transfer log. Pinned here: +// - still pending -> 200 { signature, status: 0 } with no data (no mint log yet). +// - confirmed, no mint log -> 500 naming the tx hash, NOT a confirmed response carrying +// positionAddress: '' that the caller could never address. +// - confirmed, mint log found -> 200 with the position ID from the log. +// - unexpected failure -> the underlying message survives the route's catch. + +const WETH = new Token(8453, '0x4200000000000000000000000000000000000006', 18, 'WETH', 'Wrapped Ether'); +const USDC = new Token(8453, '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', 6, 'USDC', 'USD Coin'); +const mockWallet = '0x0000000000000000000000000000000000000001'; +const poolAddress = '0xd0b53D9277642d899DF5C87A3966A349A798F224'; +const txHash = '0x2222222222222222222222222222222222222222222222222222222222222222'; +const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'; +const ZERO_TOPIC = '0x0000000000000000000000000000000000000000000000000000000000000000'; + +const { Ethereum: RealEthereum } = jest.requireActual('../../../../src/chains/ethereum/ethereum'); + +/** A live WETH/USDC 0.3% pool at ~3000 USDC per WETH. */ +const buildPool = () => { + const sqrtRatioX96 = encodeSqrtRatioX96('3000000000', '1000000000000000000'); + return new Pool(WETH, USDC, 3000, sqrtRatioX96, '1000000000000', TickMath.getTickAtSqrtRatio(sqrtRatioX96)); +}; + +const buildApp = async () => { + const server = fastifyWithTypeProvider(); + await server.register(require('@fastify/sensible')); + const { openPositionRoute } = await import('../../../../src/connectors/uniswap/clmm-routes/openPosition'); + await server.register(openPositionRoute); + return server; +}; + +const primeMocks = (receipt: any, uniswapOverrides: Record = {}) => { + (getUniswapPoolInfo as jest.Mock).mockResolvedValue({ + baseTokenAddress: WETH.address, + quoteTokenAddress: USDC.address, + }); + + (Uniswap.getInstance as jest.Mock).mockResolvedValue({ + getToken: jest.fn((address: string) => + Promise.resolve(address.toLowerCase() === WETH.address.toLowerCase() ? WETH : USDC), + ), + getV3Pool: jest.fn().mockResolvedValue(buildPool()), + ...uniswapOverrides, + }); + + const ethereum: any = { + provider: {}, + getWallet: jest.fn().mockResolvedValue({ address: mockWallet }), + getContract: jest.fn().mockReturnValue({}), + getERC20Allowance: jest.fn().mockResolvedValue({ value: BigNumber.from('1000000000000000000000000') }), + prepareGasOptions: jest.fn().mockResolvedValue({}), + handleTransactionExecution: jest.fn().mockResolvedValue(receipt), + }; + ethereum.handleTransactionConfirmation = RealEthereum.prototype.handleTransactionConfirmation.bind(ethereum); + (Ethereum.getInstance as jest.Mock).mockResolvedValue(ethereum); + + const { Contract } = require('@ethersproject/contracts'); + (Contract as jest.Mock).mockImplementation(() => ({ + multicall: jest.fn().mockResolvedValue({ hash: txHash }), + })); +}; + +const open = (server: any) => + server.inject({ + method: 'POST', + url: '/open-position', + payload: { + network: 'base', + walletAddress: mockWallet, + poolAddress, + lowerPrice: 2500, + upperPrice: 3500, + baseTokenAmount: 0.1, + quoteTokenAmount: 300, + }, + }); + +describe('POST /open-position (Uniswap V3 CLMM) — transaction confirmation', () => { + let server: any; + + beforeEach(async () => { + jest.clearAllMocks(); + (Ethereum.getWalletAddressExample as jest.Mock) = jest.fn().mockResolvedValue(mockWallet); + server = await buildApp(); + }); + + afterEach(async () => { + await server.close(); + }); + + it('returns the pending shape with the tx hash when the receipt is still missing', async () => { + primeMocks(null); + + const response = await open(server); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + expect(body.signature).toBe(txHash); + expect(body.status).toBe(0); // TransactionStatus.PENDING + expect(body.data).toBeUndefined(); + }); + + it('fails loudly naming the tx hash when the confirmed transaction has no NFT mint log', async () => { + primeMocks({ + status: 1, + transactionHash: txHash, + logs: [], // no Transfer-from-zero log -> the position ID is unknowable + gasUsed: BigNumber.from(21_000), + effectiveGasPrice: BigNumber.from('1000000000'), + }); + + const response = await open(server); + + expect(response.statusCode).toBe(500); + const body = JSON.parse(response.body); + expect(body.message).toContain(txHash); + // Never a confirmed response with an unusable position address. + expect(response.body).not.toContain('positionAddress'); + }); + + it('returns the position ID read from the NFT mint log on confirmation', async () => { + primeMocks({ + status: 1, + transactionHash: txHash, + logs: [ + { + address: getUniswapV3NftManagerAddress('base'), + topics: [TRANSFER_TOPIC, ZERO_TOPIC, ZERO_TOPIC, BigNumber.from(987654).toHexString()], + }, + ], + gasUsed: BigNumber.from(21_000), + effectiveGasPrice: BigNumber.from('1000000000'), + }); + + const response = await open(server); + + expect(response.statusCode).toBe(200); + const body = JSON.parse(response.body); + expect(body.status).toBe(1); // TransactionStatus.CONFIRMED + expect(body.data.positionAddress).toBe('987654'); + expect(body.data.fee).toBe(0.000021); + }); + + it('keeps the underlying error message instead of a bare "Failed to open position"', async () => { + primeMocks(null, { getV3Pool: jest.fn().mockRejectedValue(new Error('pool state fetch exploded')) }); + + const response = await open(server); + + expect(response.statusCode).toBe(500); + expect(JSON.parse(response.body).message).toContain('pool state fetch exploded'); + }); +});