sync / gateway development gw665 to staging - #668
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before submitting this PR, please make sure:
A description of the changes proposed in the pull request:
Tests performed by the developer:
Tips for QA testing: