Skip to content

sync / gateway development gw665 to staging - #668

Merged
rapcmia merged 24 commits into
stagingfrom
development
Jul 28, 2026
Merged

sync / gateway development gw665 to staging#668
rapcmia merged 24 commits into
stagingfrom
development

Conversation

@nikspz

@nikspz nikspz commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Before submitting this PR, please make sure:

  • Your code builds clean without any errors or warnings
  • You are using approved title ("feat/", "fix/", "docs/", "refactor/")

A description of the changes proposed in the pull request:

Tests performed by the developer:

Tips for QA testing:

fengtality and others added 24 commits July 8, 2026 21:35
…pport

Add Robinhood Chain (chainID 4663) and Robinhood Chain Testnet (chainID
46630) as Ethereum networks:
- Chain templates with EIP-1559 gas config (Arbitrum Orbit L2, ETH gas)
- Mainnet token list with 27 tokens (WETH, USDG, and Robinhood stock/ETF
  tokens), symbols and decimals verified on-chain
- Wrapped native (WETH) mapping and EIP-1559 gating for both networks

Wire up Uniswap on robinhood-chain (router, AMM, CLMM):
- Add official V2/V3/UniversalRouter deployment addresses
- Resolve the Universal Router address from the per-network contracts map
  instead of the SDK's hardcoded chain map, which throws for chains it
  doesn't know (e.g. 4663) and has no override mechanism
- Fall back to the Universal Router quote in /router/quote-swap when the
  AlphaRouter SDK doesn't support the network

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Uniswap instance mock was missing the new isAlphaRouterAvailable
method, so quoteSwap threw a TypeError and returned 500.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add Unichain (chainID 130, OP-stack L2) as an Ethereum network:
- Chain template with EIP-1559 gas config and official public RPC
- Token list with 11 major tokens from the Uniswap default token list
- Wrapped native (WETH) mapping and EIP-1559 gating
- Official Uniswap V2/V3/V4/UniversalRouter deployment addresses

Enable Uniswap v4 in /router quotes and swaps:
- Include Protocol.V4 in AlphaRouter routing on chains in the SDK's
  V4_SUPPORTED list; v4 swaps execute through the existing Universal
  Router + Permit2 flow with no API changes
- Override the SDK's on-chain quote provider on Unichain with smaller
  multicall batches (10x1.2M gas vs the default 80x1.2M), which public
  Unichain RPCs reject with intermittent ProviderGasError

Verified live: Unichain WETH->USDC quotes 3/3 via the official RPC, and
mainnet WETH->USDC now returns V3+V4 split routes when v4 wins on price.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t to never

Permit2 grants from /approve were hardcoded to expire after 48 hours, so
every uniswap/router pair silently required re-approval every 2 days --
no other connector has expiring approvals.

Add optional ethereum.permit2ExpirationSeconds chain config; when unset,
grants never expire (max uint48), matching plain ERC20 approval semantics
on other connectors. Permit2 allowances remain uint160-scoped and
revocable, so this keeps its safety advantages without the churn.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename the network identifier (and testnet) everywhere: template
filenames, config namespaces, token lists, EIP-1559 gates, WETH map,
and the Uniswap networks allow-list. The geckoId values and external
docs URLs keep their original hyphenated forms since those identifiers
are owned by CoinGecko and Uniswap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…red list

The EIP-1559 network list was duplicated in three places (twice in
ethereum.ts, once in estimate-gas.ts) and the copies drifted: unichain
was added to ethereum.ts but not estimate-gas.ts, so
GET /chains/ethereum/estimate-gas?network=unichain reported
gasType 'legacy'. Extract a single exported EIP1559_NETWORKS constant
and use it at all three sites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The route read lastGasPriceEstimate.isEIP1559 directly, but the cache
became keyed by network, so the property was always undefined and the
route never returned gasType 'eip1559' for any network. Expose a
public getCachedGasPriceEstimate() accessor on Ethereum (replacing the
(ethereum as any).constructor reach into a private static) and index
the cache correctly. EIP-1559 networks whose estimate fell back to
legacy pricing now report gasType 'legacy' instead of omitting it.

Adds route tests covering eip1559 output for mainnet/unichain/
robinhoodchain, legacy output for bsc, and the fallback case. The test
automock is patched to keep the real EIP1559_NETWORKS constant.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… V3 via QuoterV2

Two fixes to the Universal Router fallback path (used on networks the
AlphaRouter SDK doesn't support, e.g. robinhoodchain):

- quoteSwap now passes the requested slippagePct through
  getUniversalRouterQuote, so the min-out/max-in embedded in the swap
  calldata matches the minAmountOut/maxAmountIn advertised in the quote
  response. Previously the calldata always used the config slippage
  while the response reflected the caller's, so executions could revert
  inside the accepted tolerance or fill below the promised minimum.

- findV3Route now quotes every fee tier against the on-chain QuoterV2
  and keeps the best one, and getQuote picks the best route across
  V3/V2 by quoted amount. Previously it simulated locally on fabricated
  tick data (liquidityNet=0 across +/-300 tick spacings), returned the
  first fee tier with any liquidity (a dust pool could beat a deep
  one), and always preferred V3 over V2 regardless of price.

Removes tick-data-provider.test.js, which exercised the SDK's tick
machinery in support of the fabricated-tick approach. Adds route-level
tests for the fallback branch, pinning slippage threading and response
mapping.

Live-verified on robinhoodchain: SELL 0.1 WETH -> 175.4 USDG and BUY
exact-out cross-consistent; decoded calldata min-out tracks requested
slippage (1% -> 0.99%, 5% -> 4.76% implied, the SDK's quoted/(1+slip)
convention). Unichain AlphaRouter path unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…program

getMint defaults to the legacy TOKEN_PROGRAM_ID and throws on Token-2022
mints, causing swaps of those tokens to fail with "Token not found".
Read the mint account owner first and pass the correct programId.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ecute

getPool(chain, network, type, base, quote, connector?) was being called with
the connector name in the chain slot, so every amm/clmm connector failed with
"Unsupported chain: meteora" on /trading/swap/{quote,execute}.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ko4KsGwUaHjqiRTYon31Li
…ctOut route

Many thin tokens (e.g. pump.fun launches) route ExactIn on Jupiter but have
no ExactOut route, so BUY quotes failed outright. Price the requested base
amount via a reverse ExactIn quote, then fetch an executable ExactIn quote
spending that much quote token. Responses flag approximation=true and
min/max amounts are derived from the estimated (not exact) output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ko4KsGwUaHjqiRTYon31Li
The unified add schema required both baseTokenAmount and quoteTokenAmount,
but every connector supports single-sided adds (and /trading/clmm/open
already treats both as optional). Make both optional, default the omitted
side to 0, and reject only when neither is > 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ko4KsGwUaHjqiRTYon31Li
…e chokepoint

Solana.sendAndConfirmTransactionForWallet now owns wallet-type resolution
(local keypair / Ledger), fee-payer defaulting, pre-flight simulation,
compute-budget handling and extra-signer hygiene. Connectors are
wallet-type-agnostic: they build the transaction with the wallet's public
key and hand it to the chokepoint — no per-route hardware branching, no
per-route simulate. Converted routes: Jupiter router execute, Orca CLMM,
Meteora DLMM, Raydium AMM+CLMM (swaps, liquidity, positions, fees).

Ported from the QA-hardened seam on #661 (2e34d95 and follow-ups),
with the Swig wallet type stripped for this release. Fixes folded in
from that QA cycle:

- eae98ce: default a legacy tx's missing fee payer to the wallet before
  the pre-flight simulate; drop dead Raydium sign-and-send code and a
  stray console.log
- 0ad43b5: drop SDK dummy owner "signers" that carry the wallet pubkey
  (Raydium TxBuilder) instead of corrupting the wallet signature slot
- 29b376c/7a9685374: confirm until blockhash expiry instead of a fixed
  attempt count, do an authoritative final on-chain lookup before
  reporting a timeout, and surface landed-but-failed program errors
  instead of a misleading TRANSACTION_TIMEOUT
- b00320c: apply the Meteora bitmap-extension writability fix (#639) to
  SELL swaps too
- 7dd9bf1: validate DLMM position width (69-bin cap) in openPosition
- b527698: assert /trading/clmm routes dispatch to the meteora
  connector rather than a stale 404 proxy

kitInstructionToWeb3 (kit -> web3.js adapter used by Orca v4 builds)
moves to chains/solana/kit-instructions.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJFHMTAkk66Q7da1diLQqG
Ported from the swig-branch QA cycle (7dd9bf1, 0ad43b5 on #661), minus
the Swig-specific error tables: "Failed to reallocate account data" now
explains Solana's 10,240-byte CPI allocation limit and the Meteora DLMM
69-bin position cap instead of surfacing a bare instruction error.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJFHMTAkk66Q7da1diLQqG
The route only checked the encrypted keystore file, so a registered
hardware (Ledger) wallet could never be set as default. Check the
hardware-wallet registry too. Ported from 0ad43b5 on #661, minus the
Swig registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJFHMTAkk66Q7da1diLQqG
…back

e099fd5 added an ExactIn approximation when a BUY has no ExactOut route;
the old test still asserted the single-attempt behavior. Assert the new
flow: ExactOut attempt, reverse ExactIn probe, and the combined
no-route error message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJFHMTAkk66Q7da1diLQqG
…l-leg helper

Adds approximateIfNoExactOut (default true) to the base router schema's
quote-swap and execute-swap requests, and approximation to the quote
response. When a router cannot serve an ExactOut quote for a BUY, the
shared approximateBuyViaSellLeg helper derives the price from a sell-leg
ExactIn quote and returns an executable forward ExactIn quote flagged
approximation: true (slippage applies to the estimated output). With the
flag false, such BUYs fail with a clear 400.

Jupiter's 'ExactOut not supported for this token pair' error path now
falls back through the same helper, making its previously-vestigial
approximation response field meaningful.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHA8E865HgkeMmCCmy4oZm
Three new router connectors following the Jupiter standard (schema
supersets, quoteId cache, unsigned tx through the wallet-type-aware
chokepoint), registered at /connectors/<name>/router/* and in the
unified /trading/swap dispatch:

- dflow: quote-api.dflow.net (x-api-key; keyless dev endpoint without
  one). Jupiter-like GET /quote -> POST /swap base64 transaction.
  ExactIn-only — the API silently ignores swapMode (verified live), so
  BUYs always use the sell-leg approximation.
- okx: web3.okx.com v6 DEX aggregator with HMAC-signed requests
  (OK-ACCESS-* headers; query serialized once so the signature matches
  byte-for-byte), {code,msg,data[]} envelope unwrapping, native
  exactOut BUY with approximation fallback. Executable tx is
  wallet-bound, so execute-quote re-fetches the route with the wallet.
- titan: DART public JSON API (keyless 1 req/s tier, optional
  X-API-Key). Returns instructions + ALTs; titan.utils compiles an
  unsigned V0 transaction with a fresh blockhash, deduping repeated
  ComputeBudget instructions (the live API returns a duplicated
  RequestHeapFrame the runtime rejects). Quotes are wallet-bound
  (walletAddress on quote-swap, default solana.defaultWallet) and
  execution enforces a wallet match.

READMEs in the dflow/ and okx/ connector folders document how to get
API credentials. Orca's route-registration test now probes routes via
inject because printRoutes compresses the shared okx/orca prefix.

Live-verified on mainnet-beta: DFlow SELL executed on-chain
(DzBWWHx1uNYo7YAoLDZP8W4W2MtMEkxJTMdffUM5mi6yC9Bep8zkixQvcSt6WUpBtNMdVJQHLGUVVekvQH3WfpW),
Titan and DFlow quotes keyless, BUY approximation within 0.01% of
target size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHA8E865HgkeMmCCmy4oZm
…apProvider config

Two fixes surfaced by live-executing the new routers on mainnet-beta:

- prepareTx/prepareVersionedTx stripped ALL ComputeBudget instructions
  before adding their own CU limit/price, silently discarding
  RequestHeapFrame. The Titan program requires a 256KB heap frame and
  crashed on-chain with 'Access violation in heap section' without it.
  Only SetComputeUnitLimit (discriminator 2) and SetComputeUnitPrice
  (3) are replaced now; other ComputeBudget types are preserved.

- The unified /trading/swap quote and execute schemas set
  default: 'jupiter/router' on the optional connector param, so
  Fastify injected it on every request and the network's configured
  swapProvider was never consulted (also broke Ethereum unified swaps,
  which got jupiter/router injected). The default is now an example
  only, and both network schemas gained a swapProvider enum of the
  valid providers so typos fail config validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GHA8E865HgkeMmCCmy4oZm
…Default, and executeQuote routes

- sendAndConfirmTransactionForWallet: hardware-wallet path signs via
  signTransactionByType, gets the compute budget added, and keeps the
  ephemeral extra signer's signature
- prepareTx/prepareVersionedTx: replace only CU-limit/price (discriminators
  2/3) and preserve RequestHeapFrame — regression for the Titan heap-frame
  crash — plus direct isReplacedComputeBudgetInstruction cases
- /wallet/setDefault: local keystore, hardware-registry (regression for the
  ported fix), and not-found cases
- DFlow/OKX/Titan execute-quote routes: cached quote flows through the
  wallet-aware chokepoint, quote consumed only on confirmed execution,
  connector/wallet mismatches and expired quotes rejected

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WJFHMTAkk66Q7da1diLQqG
Swaps on Robinhood Chain reverted with SliceOutOfBounds() at ~25k gas.
Chains that smart-order-router does not support fall through to
UniversalRouterService, which encoded via universal-router-sdk 4.19.6 --
a version whose enum only has 1.2 and 2.0, and which does not know chain
4663 at all. Robinhood Chain has no 2.0 deployment (2.1.1 only), so we
were sending 2.0 calldata to a 2.1.1 router and it failed while decoding.

Upgrade universal-router-sdk to 5.10.0 (adds 2.1.1/2.2.0 and chain 4663)
and align sdk-core and the v2/v3/v4/router SDKs to the versions it
requires, otherwise two copies of router-sdk break typechecking.

Rather than hardcode a version, getUniversalRouterVersion() reverse-looks-
up the configured router address in the SDK's deployment map, so the
encoder version is derived from the address:
  - robinhoodchain -> 2.1.1 (was silently encoding 2.0)
  - celo           -> 1.2   (broken the same way, also fixed)
  - all others     -> 2.0   (unchanged; no re-approval needed)
An unrecognized address now throws naming the chain instead of emitting
calldata the router will reject.

The AlphaRouter path stays on 2.0 by design: smart-order-router bundles
its own older SDK that can only encode 1.2/2.0, and every chain it
supports has a 2.0 deployment.

Also fixes two bugs found on the same path:

- executeQuote threw "Invalid time value" on every swap. /approve writes
  max uint48 as the "never expires" Permit2 sentinel; multiplied by 1000
  that is outside JS Date range, so toISOString() raised RangeError. The
  allowance check itself passed -- the crash was purely in debug logging.
  Covered by a regression test verified to fail without the fix.

- Wrapping ETH ran out of gas at the hardcoded 50k limit. Robinhood
  Chain's WETH is a proxy, so deposit() needs ~58.6k. Raised to 100k.

Permit2 grants are now always unlimited; drop the unused
ethereum.permit2ExpirationSeconds option and its schema/template entries.

Verified live on Robinhood Chain (WETH-USDG, both directions):
  SELL 0.0005 WETH -> 0x485fad811b53216f25359794b4c821d60e48e098360b0529d52fa6033bfd2067
  BUY  0.0002 WETH -> 0xadd684f63a4a871af2e9e9008c09f4461557e3128706a8bdc8bc87886f35a338

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WMhQLeBUdQ86m69uPysCLg
A throttled Jupiter request reached callers as:

  No route found for <TOKEN> -> <TOKEN> (ExactIn).
  Unexpected token 'R', "Rate limit"... is not valid JSON  [NO_ROUTE_FOUND]

which reads as "this token is untradable" and has caused callers to
blacklist perfectly good pools.

Root cause is in HttpClient. Jupiter answers a throttled request with a
plain-text "Rate limit exceeded" body under an application/json
content-type. Calling response.json() on that throws a SyntaxError which
escaped to the network-error branch, discarding the 429 status - so the
parse error itself became the caller-visible message. Read the body as
text first and parse defensively, keeping status, data and headers.

On top of that:

- HttpClientError exposes isRateLimit and retryAfterSeconds.
- Jupiter quote/swap calls retry with exponential backoff while throttled,
  honoring Retry-After. Base delay reflects the tier documented at
  https://developers.jup.ag/docs/ultra/rate-limit - 1s with a portal key
  (api.jup.ag), 5s keyless (lite-api.jup.ag, far tighter). Exhausting the
  retries raises a 429 RATE_LIMITED naming the tier, rather than a routing
  error.
- quoteSwap propagates a rate limit untouched instead of relabelling it,
  and skips the ExactIn fallback, which would only burn another throttled
  request.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0154EUNSY4ocqsWPKZWgwBJn
feat: add Robinhood Chain + Unichain networks, Uniswap v4 routing, Solana signing chokepoint, and DFlow/OKX/Titan Solana router connectors
@rapcmia
rapcmia merged commit 795e549 into staging Jul 28, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants