From 58168a1b3614ab01ba17598eea6ad8f7722f208c Mon Sep 17 00:00:00 2001 From: Discostu Date: Tue, 7 Apr 2026 11:02:40 +0200 Subject: [PATCH 01/17] feat: add Stargate V2 swapper integration Integrate Stargate V2 (LayerZero) as a cross-chain bridge swapper. - 14 EVM chains supported (ETH, ARB, OP, BASE, POLYGON, BSC, AVAX, etc.) - On-chain quotes via quoteOFT/quoteSend contract calls - USDC, USDT, ETH, mETH, EURC asset support - Transaction status tracking via LayerZero scan API - Feature-flagged (disabled by default) --- .env | 7 +- headers/csps/defi/swappers/Odos.ts | 5 + headers/csps/defi/swappers/Stargate.ts | 8 + headers/csps/index.ts | 6 +- packages/swapper/src/constants.ts | 22 +- packages/swapper/src/index.ts | 1 + .../StargateSwapper/StargateSwapper.ts | 6 + .../src/swappers/StargateSwapper/constant.ts | 105 +++++ .../src/swappers/StargateSwapper/endpoints.ts | 149 +++++++ .../getTradeQuote/getTradeQuote.ts | 46 +++ .../getTradeRate/getTradeRate.ts | 24 ++ .../src/swappers/StargateSwapper/index.ts | 2 + .../src/swappers/StargateSwapper/types.ts | 30 ++ .../utils/fetchStargateTrade.ts | 363 ++++++++++++++++++ .../swappers/StargateSwapper/utils/helpers.ts | 171 +++++++++ .../StargateSwapper/utils/stargateService.ts | 15 + packages/swapper/src/types.ts | 13 +- .../components/SwapperIcon/SwapperIcon.tsx | 8 +- .../components/SwapperIcon/odos-icon.png | Bin 0 -> 42072 bytes .../components/SwapperIcon/stargate-icon.png | Bin 0 -> 7085 bytes src/config.ts | 5 +- src/state/helpers.ts | 11 +- .../preferencesSlice/preferencesSlice.ts | 6 +- src/test/mocks/store.ts | 4 +- 24 files changed, 998 insertions(+), 9 deletions(-) create mode 100644 headers/csps/defi/swappers/Odos.ts create mode 100644 headers/csps/defi/swappers/Stargate.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/StargateSwapper.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/constant.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/endpoints.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/getTradeRate/getTradeRate.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/index.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/types.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/utils/helpers.ts create mode 100644 packages/swapper/src/swappers/StargateSwapper/utils/stargateService.ts create mode 100644 src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/odos-icon.png create mode 100644 src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/stargate-icon.png diff --git a/.env b/.env index 5f424cb4679..112cfc41381 100644 --- a/.env +++ b/.env @@ -116,6 +116,8 @@ VITE_FEATURE_TON=true VITE_FEATURE_EARN_TAB=true VITE_FEATURE_ACROSS_SWAP=true VITE_FEATURE_DEBRIDGE_SWAP=true +VITE_FEATURE_ODOS_SWAP=false +VITE_FEATURE_STARGATE_SWAP=false VITE_FEATURE_USERBACK=true VITE_FEATURE_AGENTIC_CHAT=false VITE_FEATURE_MM_NATIVE_MULTICHAIN=false @@ -360,8 +362,11 @@ VITE_ACROSS_INTEGRATOR_ID= # deBridge DLN VITE_DEBRIDGE_API_URL=https://dln.debridge.finance/v1.0 +# Odos +VITE_ODOS_API_URL=https://api.odos.xyz + # Userback feedback widget VITE_USERBACK_TOKEN=A-3gHopRTd55QqxXGsJd0XLVVG3 # agentic chat -VITE_AGENTIC_SERVER_BASE_URL=https://api.agent.shapeshift.com +VITE_AGENTIC_SERVER_BASE_URL=https://api.agent.shapeshift.com \ No newline at end of file diff --git a/headers/csps/defi/swappers/Odos.ts b/headers/csps/defi/swappers/Odos.ts new file mode 100644 index 00000000000..27dfc347b8f --- /dev/null +++ b/headers/csps/defi/swappers/Odos.ts @@ -0,0 +1,5 @@ +import type { Csp } from '../../../types' + +export const csp: Csp = { + 'connect-src': ['https://api.odos.xyz'], +} diff --git a/headers/csps/defi/swappers/Stargate.ts b/headers/csps/defi/swappers/Stargate.ts new file mode 100644 index 00000000000..c76f9eab4ff --- /dev/null +++ b/headers/csps/defi/swappers/Stargate.ts @@ -0,0 +1,8 @@ +import type { Csp } from '../../../types' + +export const csp: Csp = { + 'connect-src': [ + 'https://mainnet.stargate-api.com', + 'https://scan.layerzero-api.com', + ], +} diff --git a/headers/csps/index.ts b/headers/csps/index.ts index 93352adb685..e5973cb9963 100644 --- a/headers/csps/index.ts +++ b/headers/csps/index.ts @@ -65,7 +65,9 @@ import { csp as bebop } from './defi/swappers/Bebop' import { csp as butterSwap } from './defi/swappers/ButterSwap' import { csp as cowSwap } from './defi/swappers/CowSwap' import { csp as nearIntents } from './defi/swappers/NearIntents' +import { csp as odos } from './defi/swappers/Odos' import { csp as oneInch } from './defi/swappers/OneInch' +import { csp as stargate } from './defi/swappers/Stargate' import { csp as portals } from './defi/swappers/Portals' import { csp as stonfi } from './defi/swappers/Stonfi' import { csp as sunio } from './defi/swappers/Sunio' @@ -187,12 +189,14 @@ export const csps = [ bebop, cowSwap, nearIntents, + odos, oneInch, portals, stonfi, sunio, thor, butterSwap, + stargate, foxPage, walletConnectToDapps, coinbase, @@ -206,4 +210,4 @@ export const csps = [ railway, discord, yieldxyz, -] +] \ No newline at end of file diff --git a/packages/swapper/src/constants.ts b/packages/swapper/src/constants.ts index 40f5536380d..3554f670596 100644 --- a/packages/swapper/src/constants.ts +++ b/packages/swapper/src/constants.ts @@ -22,6 +22,10 @@ import { mayachainApi } from './swappers/MayachainSwapper/endpoints' import { mayachainSwapper } from './swappers/MayachainSwapper/MayachainSwapper' import { nearIntentsApi } from './swappers/NearIntentsSwapper/endpoints' import { nearIntentsSwapper } from './swappers/NearIntentsSwapper/NearIntentsSwapper' +import { odosApi } from './swappers/OdosSwapper/endpoints' +import { odosSwapper } from './swappers/OdosSwapper/OdosSwapper' +import { stargateApi } from './swappers/StargateSwapper/endpoints' +import { stargateSwapper } from './swappers/StargateSwapper/StargateSwapper' import { portalsApi } from './swappers/PortalsSwapper/endpoints' import { portalsSwapper } from './swappers/PortalsSwapper/PortalsSwapper' import { relaySwapper } from './swappers/RelaySwapper' @@ -116,6 +120,14 @@ export const swappers: Record = ...debridgeSwapper, ...debridgeApi, }, + [SwapperName.Odos]: { + ...odosSwapper, + ...odosApi, + }, + [SwapperName.Stargate]: { + ...stargateSwapper, + ...stargateApi, + }, [SwapperName.Test]: undefined, } @@ -135,6 +147,7 @@ const DEFAULT_AVNU_SLIPPAGE_DECIMAL_PERCENTAGE = '0.02' const DEFAULT_STONFI_SLIPPAGE_DECIMAL_PERCENTAGE = '0.01' // deBridge API off-chain simulation overestimates output on some chains (e.g. SEI ~2.4%), so auto slippage (1%) is insufficient const DEFAULT_DEBRIDGE_SLIPPAGE_DECIMAL_PERCENTAGE = '0.03' +const DEFAULT_STARGATE_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' export const getDefaultSlippageDecimalPercentageForSwapper = ( swapperName: SwapperName | undefined, @@ -167,6 +180,8 @@ export const getDefaultSlippageDecimalPercentageForSwapper = ( return DEFAULT_BUTTERSWAP_SLIPPAGE_DECIMAL_PERCENTAGE case SwapperName.NearIntents: return DEFAULT_NEAR_INTENTS_SLIPPAGE_DECIMAL_PERCENTAGE + case SwapperName.Odos: + return '0.003' case SwapperName.Cetus: return DEFAULT_CETUS_SLIPPAGE_DECIMAL_PERCENTAGE case SwapperName.Sunio: @@ -175,6 +190,8 @@ export const getDefaultSlippageDecimalPercentageForSwapper = ( return DEFAULT_AVNU_SLIPPAGE_DECIMAL_PERCENTAGE case SwapperName.Stonfi: return DEFAULT_STONFI_SLIPPAGE_DECIMAL_PERCENTAGE + case SwapperName.Stargate: + return DEFAULT_STARGATE_SLIPPAGE_DECIMAL_PERCENTAGE default: return assertUnreachable(swapperName) } @@ -186,7 +203,10 @@ export const isAutoSlippageSupportedBySwapper = (swapperName: SwapperName): bool case SwapperName.Across: case SwapperName.Debridge: return true + case SwapperName.Odos: + case SwapperName.Stargate: + return false default: return false } -} +} \ No newline at end of file diff --git a/packages/swapper/src/index.ts b/packages/swapper/src/index.ts index 33e507c1c0b..ab775ae4a6a 100644 --- a/packages/swapper/src/index.ts +++ b/packages/swapper/src/index.ts @@ -11,6 +11,7 @@ export * from './swappers/SunioSwapper' export * from './swappers/CowSwapper' export * from './swappers/DebridgeSwapper' export * from './swappers/PortalsSwapper' +export * from './swappers/StargateSwapper' export * from './swappers/ThorchainSwapper' export * from './swappers/MayachainSwapper' export * from './swappers/ButterSwap' diff --git a/packages/swapper/src/swappers/StargateSwapper/StargateSwapper.ts b/packages/swapper/src/swappers/StargateSwapper/StargateSwapper.ts new file mode 100644 index 00000000000..9b4fa914f86 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/StargateSwapper.ts @@ -0,0 +1,6 @@ +import type { Swapper } from '../../types' +import { executeEvmTransaction } from '../../utils' + +export const stargateSwapper: Swapper = { + executeEvmTransaction, +} diff --git a/packages/swapper/src/swappers/StargateSwapper/constant.ts b/packages/swapper/src/swappers/StargateSwapper/constant.ts new file mode 100644 index 00000000000..6c8bf497b32 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/constant.ts @@ -0,0 +1,105 @@ +import type { ChainId } from '@shapeshiftoss/caip' +import { + arbitrumChainId, + avalancheChainId, + baseChainId, + blastChainId, + bscChainId, + ethChainId, + gnosisChainId, + lineaChainId, + mantleChainId, + optimismChainId, + polygonChainId, + scrollChainId, + sonicChainId, +} from '@shapeshiftoss/caip' + +const metisChainId: ChainId = 'eip155:1088' as ChainId + +export const chainIdToStargateEndpointId = { + [ethChainId]: 30101, + [arbitrumChainId]: 30110, + [optimismChainId]: 30111, + [baseChainId]: 30184, + [polygonChainId]: 30109, + [bscChainId]: 30102, + [avalancheChainId]: 30106, + [scrollChainId]: 30214, + [lineaChainId]: 30183, + [mantleChainId]: 30181, + [gnosisChainId]: 30145, + [metisChainId]: 30151, + [sonicChainId]: 30332, + [blastChainId]: 30243, +} as const satisfies Record + +export const STARGATE_SUPPORTED_CHAIN_IDS: ChainId[] = Object.keys( + chainIdToStargateEndpointId, +) as ChainId[] + +export const stargateContractsByChainAndAsset: Record> = { + [ethChainId]: { + '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48': '0xc026395860Db2d07ee33e05fE50ed7bD583189C7', + '0xdac17f958d2ee523a2206206994597c13d831ec7': '0x933597a323Eb81cAe705C5bC29985172fd5A3973', + '0x0000000000000000000000000000000000000000': '0x77b2043768d28E9C9aB44E1aBfC95944bcE57931', + '0x9e32b13ce7f2e80a01932b42553652e053d6ed8e': '0xcDafB1b2dB43f366E48e6F614b8DCCBFeeFEEcD3', + '0xd5f7838f5c461feff7fe49ea5ebaf7728bb0adfa': '0x268Ca24DAefF1FaC2ed883c598200CcbB79E931D', + '0x1abaea1f7c830bd89acc67ec4af516284b1bc33c': '0x783129E4d7bA0Af0C896c239E57C06DF379aAE8c', + }, + [arbitrumChainId]: { + '0xaf88d065e77c8cc2239327c5edb3a432268e5831': '0xe8CDF27AcD73a434D661C84887215F7598e7d0d3', + '0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9': '0xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0', + '0x0000000000000000000000000000000000000000': '0xA45B5130f36CDcA45667738e2a258AB09f4A5f7F', + }, + [optimismChainId]: { + '0x0b2c639c533813f4aa9d7837caf62653d097ff85': '0xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0', + '0x94b008aa00579c1307b0ef2c499ad98a8ce58e58': '0x19cFCE47eD54a88614648DC3f19A5980097007dD', + '0x0000000000000000000000000000000000000000': '0xe8CDF27AcD73a434D661C84887215F7598e7d0d3', + }, + [baseChainId]: { + '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913': '0x27a16dc786820B16E5c9028b75B99F6f604b5d26', + '0x0000000000000000000000000000000000000000': '0xdc181Bd607330aeeBEF6ea62e03e5e1Fb4B6F7C7', + }, + [polygonChainId]: { + '0x3c499c542cef5e3811e1192ce70d8cc03d5c3359': '0x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4', + '0xc2132d05d31c914a87c6611c10748aeb04b58e8f': '0xd47b03ee6d86Cf251ee7860FB2ACf9f91B9fD4d7', + }, + [bscChainId]: { + '0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d': '0x962Bd449E630b0d928f308Ce63f1A21F02576057', + '0x55d398326f99059ff775485246999027b3197955': '0x138EB30f73BC423c6455C53df6D89CB01d9eBc63', + }, + [avalancheChainId]: { + '0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e': '0x5634c4a5FEd09819E3c46D86A965Dd9447d86e47', + '0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7': '0x12dC9256Acc9895B076f6638D628382881e62CeE', + }, + [scrollChainId]: { + '0x06efdbff2a14a7c8e15944d1f4a48f9f95f663a4': '0x3Fc69CC4A842838bCDC9499178740226062b14E4', + '0x0000000000000000000000000000000000000000': '0xC2b638Cb5042c1B3c5d5C969361fB50569840583', + }, + [lineaChainId]: { + '0x0000000000000000000000000000000000000000': '0x81F6138153d473E8c5EcebD3DC8Cd4903506B075', + }, + [mantleChainId]: { + '0x09bc4e0d864854c6afb6eb9a9cdf58ac190d0df9': '0xAc290Ad4e0c891FDc295ca4F0a6214cf6dC6acDC', + '0x201eba5cc46d216ce6dc03f6a759e8e766e956ae': '0xB715B85682B731dB9D5063187C450095c91C57FC', + '0xdeaddeaddeaddeaddeaddeaddeaddeaddead1111': '0x4c1d3Fc3fC3c177c3b633427c2F769276c547463', + '0xcda86a272531e8640cd7f1a92c01839911b90bb0': '0xF7628d84a2BbD9bb9c8E686AC95BB5d55169F3F1', + }, + [gnosisChainId]: { + '0x2a22f9c3b484c3629090feed35f17ff8f88f76f0': '0xB1EeAD6959cb5bB9B20417d6689922523B2B86C3', + '0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1': '0xe9aBA835f813ca05E50A6C0ce65D0D74390F7dE7', + }, + [metisChainId]: { + '0xbb06dca3ae6887fabf931640f67cab3e3a16f4dc': '0x4dCBFC0249e8d5032F89D6461218a9D2eFff5125', + '0x420000000000000000000000000000000000000a': '0x36ed193dc7160D3858EC250e69D12B03Ca087D08', + '0xdeaddeaddeaddeaddeaddeaddeaddeaddead0000': '0xD9050e7043102a0391F81462a3916326F86331F0', + }, + [sonicChainId]: { + '0x29219dd400f2bf60e5a23d13be72b486d4038894': '0xA272fFe20cFfe769CdFc4b63088DCD2C82a2D8F9', + }, +} + +export const STARGATE_NATIVE_ASSET_ADDRESS = '0x0000000000000000000000000000000000000000' + +export const DEFAULT_STARGATE_USER_ADDRESS = '0x0000000000000000000000000000000000000dead' diff --git a/packages/swapper/src/swappers/StargateSwapper/endpoints.ts b/packages/swapper/src/swappers/StargateSwapper/endpoints.ts new file mode 100644 index 00000000000..ab54e5a586d --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/endpoints.ts @@ -0,0 +1,149 @@ +import { evm, isEvmChainId } from '@shapeshiftoss/chain-adapters' +import { TxStatus } from '@shapeshiftoss/unchained-client' +import BigNumber from 'bignumber.js' + +import type { SwapperApi } from '../../types' +import { checkEvmSwapStatus, getExecutableTradeStep, isExecutableTradeQuote } from '../../utils' +import { getTradeQuote } from './getTradeQuote/getTradeQuote' +import { getTradeRate } from './getTradeRate/getTradeRate' +import { stargateService } from './utils/stargateService' + +type LayerZeroMessageStatus = 'INFLIGHT' | 'CONFIRMING' | 'DELIVERED' | 'FAILED' + +type LayerZeroMessage = { + status: LayerZeroMessageStatus + dstTxHash: string | undefined +} + +type LayerZeroScanResponse = { + messages: LayerZeroMessage[] +} + +export const stargateApi: SwapperApi = { + getTradeQuote: (input, deps) => getTradeQuote(input, deps), + getTradeRate: (input, deps) => getTradeRate(input, deps), + getEvmTransactionFees: async ({ + from, + stepIndex, + tradeQuote, + supportsEIP1559, + assertGetEvmChainAdapter, + }) => { + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const step = getExecutableTradeStep(tradeQuote, stepIndex) + + const { stargateTransactionMetadata, sellAsset } = step + if (!stargateTransactionMetadata) throw new Error('Missing Stargate transaction metadata') + + const { to, value, data } = stargateTransactionMetadata + + const adapter = assertGetEvmChainAdapter(sellAsset.chainId) + + const feeData = await evm.getFees({ adapter, data, to, value, from, supportsEIP1559 }) + + return feeData.networkFeeCryptoBaseUnit + }, + getUnsignedEvmTransaction: async ({ + from, + stepIndex, + tradeQuote, + supportsEIP1559, + assertGetEvmChainAdapter, + }) => { + if (!isExecutableTradeQuote(tradeQuote)) throw new Error('Unable to execute a trade rate quote') + + const step = getExecutableTradeStep(tradeQuote, stepIndex) + + const { accountNumber, stargateTransactionMetadata, sellAsset } = step + if (!stargateTransactionMetadata) throw new Error('Missing Stargate transaction metadata') + + const { to, value, data, gasLimit: gasLimitFromApi } = stargateTransactionMetadata + + const adapter = assertGetEvmChainAdapter(sellAsset.chainId) + + const feeData = await evm.getFees({ adapter, data, to, value, from, supportsEIP1559 }) + + const unsignedTx = await adapter.buildCustomApiTx({ + accountNumber, + data, + from, + to, + value, + ...feeData, + gasLimit: BigNumber.max(gasLimitFromApi ?? '0', feeData.gasLimit).toFixed(), + }) + + return unsignedTx + }, + checkTradeStatus: async ({ + txHash, + chainId, + address, + assertGetEvmChainAdapter, + fetchIsSmartContractAddressQuery, + }) => { + if (isEvmChainId(chainId)) { + const sourceTxStatus = await checkEvmSwapStatus({ + txHash, + chainId, + address, + assertGetEvmChainAdapter, + fetchIsSmartContractAddressQuery, + }) + + if (sourceTxStatus.status !== TxStatus.Confirmed) return sourceTxStatus + } + + const maybeStatusResponse = await stargateService.get( + `https://scan.layerzero-api.com/v1/messages/tx/${txHash}`, + ) + + if (maybeStatusResponse.isErr()) { + return { + buyTxHash: undefined, + status: TxStatus.Pending, + message: undefined, + } + } + + const { data: statusResponse } = maybeStatusResponse.unwrap() + + const firstMessage = statusResponse.messages[0] as LayerZeroMessage | undefined + + const status = (() => { + switch (firstMessage?.status) { + case 'INFLIGHT': + case 'CONFIRMING': + return TxStatus.Pending + case 'DELIVERED': + return TxStatus.Confirmed + case 'FAILED': + return TxStatus.Failed + default: + return TxStatus.Pending + } + })() + + const message = (() => { + switch (firstMessage?.status) { + case 'INFLIGHT': + return 'Cross-chain message in flight...' + case 'CONFIRMING': + return 'Confirming on destination chain...' + case 'FAILED': + return 'Cross-chain transfer failed' + default: + return undefined + } + })() + + const buyTxHash = firstMessage?.dstTxHash + + return { + status, + buyTxHash, + message, + } + }, +} diff --git a/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.ts new file mode 100644 index 00000000000..a88cb988190 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.ts @@ -0,0 +1,46 @@ +import type { Result } from '@sniptt/monads' +import { Err } from '@sniptt/monads' + +import type { CommonTradeQuoteInput, SwapErrorRight, SwapperDeps, TradeQuote } from '../../../types' +import { makeSwapErrorRight } from '../../../utils' +import { fetchStargateTrade } from '../utils/fetchStargateTrade' + +export const getTradeQuote = ( + input: CommonTradeQuoteInput, + deps: SwapperDeps, +): Promise> => { + if (!input.sendAddress) { + return Promise.resolve( + Err( + makeSwapErrorRight({ + message: 'sendAddress is required', + }), + ), + ) + } + + if (!input.receiveAddress) { + return Promise.resolve( + Err( + makeSwapErrorRight({ + message: 'receiveAddress is required', + }), + ), + ) + } + + const args = { + buyAsset: input.buyAsset, + receiveAddress: input.receiveAddress, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + input.sellAmountIncludingProtocolFeesCryptoBaseUnit, + sellAsset: input.sellAsset, + sendAddress: input.sendAddress, + quoteOrRate: 'quote' as const, + accountNumber: input.accountNumber, + affiliateBps: input.affiliateBps, + slippageTolerancePercentageDecimal: input.slippageTolerancePercentageDecimal, + } + + return fetchStargateTrade({ input: args, deps }) +} diff --git a/packages/swapper/src/swappers/StargateSwapper/getTradeRate/getTradeRate.ts b/packages/swapper/src/swappers/StargateSwapper/getTradeRate/getTradeRate.ts new file mode 100644 index 00000000000..d53f84d4022 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/getTradeRate/getTradeRate.ts @@ -0,0 +1,24 @@ +import type { Result } from '@sniptt/monads' + +import type { GetTradeRateInput, SwapErrorRight, SwapperDeps, TradeRate } from '../../../types' +import { fetchStargateTrade } from '../utils/fetchStargateTrade' + +export const getTradeRate = ( + input: GetTradeRateInput, + deps: SwapperDeps, +): Promise> => { + const args = { + quoteOrRate: 'rate' as const, + buyAsset: input.buyAsset, + receiveAddress: input.receiveAddress, + sellAmountIncludingProtocolFeesCryptoBaseUnit: + input.sellAmountIncludingProtocolFeesCryptoBaseUnit, + sellAsset: input.sellAsset, + sendAddress: input.sendAddress, + accountNumber: input.accountNumber, + affiliateBps: input.affiliateBps, + slippageTolerancePercentageDecimal: input.slippageTolerancePercentageDecimal, + } + + return fetchStargateTrade({ input: args, deps }) +} diff --git a/packages/swapper/src/swappers/StargateSwapper/index.ts b/packages/swapper/src/swappers/StargateSwapper/index.ts new file mode 100644 index 00000000000..200c41f3622 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/index.ts @@ -0,0 +1,2 @@ +export { stargateSwapper } from './StargateSwapper' +export { stargateApi } from './endpoints' diff --git a/packages/swapper/src/swappers/StargateSwapper/types.ts b/packages/swapper/src/swappers/StargateSwapper/types.ts new file mode 100644 index 00000000000..1d1122cb382 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/types.ts @@ -0,0 +1,30 @@ +import type { Address, Hex } from 'viem' + +export type StargateTransactionMetadata = { + to: Address + data: Hex + value: string + gasLimit: string +} + +export type StargateSendParam = { + dstEid: number + to: Hex + amountLD: bigint + minAmountLD: bigint + extraOptions: Hex + composeMsg: Hex + oftCmd: Hex +} + +export type StargateQuoteOFTResponse = { + minAmountLD: bigint + maxAmountLD: bigint + detailDstAmountLD: bigint + detailFeeAmountLD: bigint +} + +export type StargateMessagingFee = { + nativeFee: bigint + lzTokenFee: bigint +} diff --git a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts new file mode 100644 index 00000000000..c2b2f491fe7 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts @@ -0,0 +1,363 @@ +import type { AssetId, ChainId } from '@shapeshiftoss/caip' +import { fromAssetId } from '@shapeshiftoss/caip' +import { evm } from '@shapeshiftoss/chain-adapters' +import { viemClientByChainId } from '@shapeshiftoss/contracts' +import type { Result } from '@sniptt/monads' +import { Err, Ok } from '@sniptt/monads' +import BigNumber from 'bignumber.js' +import type { Address, Hex } from 'viem' +import { pad } from 'viem' + +import type { + SwapErrorRight, + SwapperDeps, + TradeQuote, + TradeQuoteStep, + TradeRate, + TradeRateStep, +} from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { getInputOutputRate, makeSwapErrorRight } from '../../../utils' +import { isNativeEvmAsset } from '../../utils/helpers/helpers' +import { + chainIdToStargateEndpointId, + DEFAULT_STARGATE_USER_ADDRESS, + STARGATE_NATIVE_ASSET_ADDRESS, + STARGATE_SUPPORTED_CHAIN_IDS, + stargateContractsByChainAndAsset, +} from '../constant' +import type { StargateMessagingFee, StargateSendParam, StargateTransactionMetadata } from '../types' +import { + decodeQuoteOFTResult, + decodeQuoteSendResult, + encodeQuoteOFT, + encodeQuoteSend, + encodeSend, +} from './helpers' + +type StargateTradeInput = { + sellAsset: { + assetId: AssetId + chainId: ChainId + precision: number + symbol: string + } & Record + buyAsset: { + assetId: AssetId + chainId: ChainId + precision: number + symbol: string + } & Record + sellAmountIncludingProtocolFeesCryptoBaseUnit: string + sendAddress: T extends 'quote' ? string : string | undefined + receiveAddress: T extends 'quote' ? string : string | undefined + accountNumber: T extends 'quote' ? number : undefined + affiliateBps: string + slippageTolerancePercentageDecimal?: string + quoteOrRate: T +} + +const getStargateAssetAddress = (assetId: AssetId): string => { + if (isNativeEvmAsset(assetId)) return STARGATE_NATIVE_ASSET_ADDRESS + const { assetReference } = fromAssetId(assetId) + return assetReference.toLowerCase() +} + +export async function fetchStargateTrade(args: { + input: StargateTradeInput<'quote'> + deps: SwapperDeps +}): Promise> + +export async function fetchStargateTrade(args: { + input: StargateTradeInput<'rate'> + deps: SwapperDeps +}): Promise> + +export async function fetchStargateTrade({ + input, + deps, +}: { + input: StargateTradeInput + deps: SwapperDeps +}): Promise> { + const { + sellAsset, + buyAsset, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + accountNumber, + affiliateBps, + } = input + + if (sellAsset.chainId === buyAsset.chainId) { + return Err( + makeSwapErrorRight({ + message: 'Stargate does not support same-chain swaps', + code: TradeQuoteError.UnsupportedTradePair, + }), + ) + } + + if (!STARGATE_SUPPORTED_CHAIN_IDS.includes(sellAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: `Sell asset chain '${sellAsset.chainId}' not supported by Stargate`, + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + if (!STARGATE_SUPPORTED_CHAIN_IDS.includes(buyAsset.chainId)) { + return Err( + makeSwapErrorRight({ + message: `Buy asset chain '${buyAsset.chainId}' not supported by Stargate`, + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + const sellAssetAddress = getStargateAssetAddress(sellAsset.assetId) + const chainContracts = stargateContractsByChainAndAsset[sellAsset.chainId] + const contractAddress = chainContracts?.[sellAssetAddress] as Address | undefined + + if (!contractAddress) { + return Err( + makeSwapErrorRight({ + message: `No Stargate contract found for asset ${sellAsset.assetId} on chain ${sellAsset.chainId}`, + code: TradeQuoteError.UnsupportedTradePair, + }), + ) + } + + const dstEid = + chainIdToStargateEndpointId[buyAsset.chainId as keyof typeof chainIdToStargateEndpointId] + + if (dstEid === undefined) { + return Err( + makeSwapErrorRight({ + message: `No LayerZero endpoint ID found for chain ${buyAsset.chainId}`, + code: TradeQuoteError.UnsupportedChain, + }), + ) + } + + const receiveAddress = + input.quoteOrRate === 'rate' + ? input.receiveAddress ?? DEFAULT_STARGATE_USER_ADDRESS + : input.receiveAddress + + const sendAddress = + input.quoteOrRate === 'rate' + ? input.sendAddress ?? DEFAULT_STARGATE_USER_ADDRESS + : input.sendAddress + + if (!receiveAddress) { + return Err( + makeSwapErrorRight({ + message: 'Receive address is required', + code: TradeQuoteError.InternalError, + }), + ) + } + + if (!sendAddress) { + return Err( + makeSwapErrorRight({ + message: 'Send address is required', + code: TradeQuoteError.InternalError, + }), + ) + } + + const paddedReceiveAddress = pad(receiveAddress as Hex, { size: 32 }) + const amountLD = BigInt(sellAmountIncludingProtocolFeesCryptoBaseUnit) + + const sendParam: StargateSendParam = { + dstEid, + to: paddedReceiveAddress, + amountLD, + minAmountLD: 0n, + extraOptions: '0x' as Hex, + composeMsg: '0x' as Hex, + oftCmd: '0x' as Hex, + } + + const publicClient = viemClientByChainId[sellAsset.chainId] + + if (!publicClient) { + return Err( + makeSwapErrorRight({ + message: `No public client found for chain ${sellAsset.chainId}`, + code: TradeQuoteError.InternalError, + }), + ) + } + + try { + const quoteOFTCalldata = encodeQuoteOFT(sendParam) + + const quoteOFTResult = await publicClient.call({ + to: contractAddress, + data: quoteOFTCalldata, + }) + + if (!quoteOFTResult.data) { + return Err( + makeSwapErrorRight({ + message: 'quoteOFT returned no data', + code: TradeQuoteError.NoRouteFound, + }), + ) + } + + const [_limit, _oftFeeDetails, receipt] = decodeQuoteOFTResult(quoteOFTResult.data as Hex) + + const detailDstAmountLD = receipt.amountReceivedLD + const detailFeeAmountLD = receipt.amountSentLD - receipt.amountReceivedLD + + const quoteSendCalldata = encodeQuoteSend(sendParam, false) + + const quoteSendResult = await publicClient.call({ + to: contractAddress, + data: quoteSendCalldata, + }) + + if (!quoteSendResult.data) { + return Err( + makeSwapErrorRight({ + message: 'quoteSend returned no data', + code: TradeQuoteError.NoRouteFound, + }), + ) + } + + const messagingFee: StargateMessagingFee = decodeQuoteSendResult( + quoteSendResult.data as Hex, + ) as unknown as StargateMessagingFee + + const buyAmountAfterFeesCryptoBaseUnit = detailDstAmountLD.toString() + const buyAmountBeforeFeesCryptoBaseUnit = (detailDstAmountLD + detailFeeAmountLD).toString() + const protocolFeeAmountCryptoBaseUnit = detailFeeAmountLD.toString() + const nativeFeeCryptoBaseUnit = messagingFee.nativeFee.toString() + + const rate = getInputOutputRate({ + sellAmountCryptoBaseUnit: sellAmountIncludingProtocolFeesCryptoBaseUnit, + buyAmountCryptoBaseUnit: buyAmountAfterFeesCryptoBaseUnit, + sellAsset: sellAsset as Parameters[0]['sellAsset'], + buyAsset: buyAsset as Parameters[0]['buyAsset'], + }) + + const sendCalldata = encodeSend( + sendParam, + { nativeFee: messagingFee.nativeFee, lzTokenFee: messagingFee.lzTokenFee }, + sendAddress as Hex, + ) + + const isNative = isNativeEvmAsset(sellAsset.assetId) + const txValue = isNative + ? new BigNumber(nativeFeeCryptoBaseUnit) + .plus(sellAmountIncludingProtocolFeesCryptoBaseUnit) + .toFixed(0) + : nativeFeeCryptoBaseUnit + + const adapter = deps.assertGetEvmChainAdapter(sellAsset.chainId) + const { average } = await adapter.getGasFeeData() + const supportsEIP1559 = 'maxFeePerGas' in average + + const networkFeeCryptoBaseUnit = await (async () => { + try { + const feeData = await evm.getFees({ + adapter, + data: sendCalldata, + to: contractAddress, + value: txValue, + from: sendAddress, + supportsEIP1559, + }) + return feeData.networkFeeCryptoBaseUnit + } catch { + return evm.calcNetworkFeeCryptoBaseUnit({ + ...average, + supportsEIP1559, + gasLimit: '500000', + }) + } + })() + + const stargateTransactionMetadata: StargateTransactionMetadata = { + to: contractAddress, + data: sendCalldata, + value: txValue, + gasLimit: '500000', + } + + const protocolFees: Record< + AssetId, + { + amountCryptoBaseUnit: string + asset: { symbol: string; chainId: ChainId; precision: number } + requiresBalance: boolean + } + > = { + [sellAsset.assetId]: { + amountCryptoBaseUnit: protocolFeeAmountCryptoBaseUnit, + asset: { + symbol: sellAsset.symbol, + chainId: sellAsset.chainId, + precision: sellAsset.precision, + }, + requiresBalance: false, + }, + } + + const step: TradeQuoteStep | TradeRateStep = { + allowanceContract: contractAddress, + rate, + buyAmountBeforeFeesCryptoBaseUnit, + buyAmountAfterFeesCryptoBaseUnit, + sellAmountIncludingProtocolFeesCryptoBaseUnit, + buyAsset: buyAsset as TradeQuoteStep['buyAsset'], + sellAsset: sellAsset as TradeQuoteStep['sellAsset'], + accountNumber: accountNumber as number | undefined, + feeData: { + networkFeeCryptoBaseUnit, + protocolFees, + }, + source: SwapperName.Stargate, + estimatedExecutionTimeMs: 60_000, + stargateTransactionMetadata, + } + + const baseQuoteOrRate = { + id: `stargate-${sellAsset.chainId}-${buyAsset.chainId}-${Date.now()}`, + rate, + swapperName: SwapperName.Stargate, + affiliateBps, + slippageTolerancePercentageDecimal: input.slippageTolerancePercentageDecimal, + } + + if (input.quoteOrRate === 'quote') { + const tradeQuote: TradeQuote = { + ...baseQuoteOrRate, + steps: [step as TradeQuoteStep], + receiveAddress, + quoteOrRate: 'quote' as const, + } + return Ok([tradeQuote]) + } + + const tradeRate: TradeRate = { + ...baseQuoteOrRate, + steps: [step as TradeRateStep], + receiveAddress, + quoteOrRate: 'rate' as const, + } + return Ok([tradeRate]) + } catch (e) { + return Err( + makeSwapErrorRight({ + message: `Stargate quote failed: ${e instanceof Error ? e.message : String(e)}`, + code: TradeQuoteError.NoRouteFound, + }), + ) + } +} diff --git a/packages/swapper/src/swappers/StargateSwapper/utils/helpers.ts b/packages/swapper/src/swappers/StargateSwapper/utils/helpers.ts new file mode 100644 index 00000000000..1fb4f3d5e0c --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/utils/helpers.ts @@ -0,0 +1,171 @@ +import type { Abi, Hex } from 'viem' +import { decodeFunctionResult, encodeFunctionData } from 'viem' + +import type { StargateSendParam } from '../types' + +const SendParamTuple = { + type: 'tuple', + name: 'sendParam', + components: [ + { name: 'dstEid', type: 'uint32' }, + { name: 'to', type: 'bytes32' }, + { name: 'amountLD', type: 'uint256' }, + { name: 'minAmountLD', type: 'uint256' }, + { name: 'extraOptions', type: 'bytes' }, + { name: 'composeMsg', type: 'bytes' }, + { name: 'oftCmd', type: 'bytes' }, + ], +} as const + +export const IStargateAbi = [ + { + name: 'quoteOFT', + type: 'function', + stateMutability: 'view', + inputs: [SendParamTuple], + outputs: [ + { + type: 'tuple', + name: 'limit', + components: [ + { name: 'minAmountLD', type: 'uint256' }, + { name: 'maxAmountLD', type: 'uint256' }, + ], + }, + { + type: 'tuple[]', + name: 'oftFeeDetails', + components: [ + { name: 'feeAmountLD', type: 'int256' }, + { name: 'description', type: 'string' }, + ], + }, + { + type: 'tuple', + name: 'receipt', + components: [ + { name: 'amountSentLD', type: 'uint256' }, + { name: 'amountReceivedLD', type: 'uint256' }, + ], + }, + ], + }, + { + name: 'quoteSend', + type: 'function', + stateMutability: 'view', + inputs: [SendParamTuple, { name: '_payInLzToken', type: 'bool' }], + outputs: [ + { + type: 'tuple', + name: 'fee', + components: [ + { name: 'nativeFee', type: 'uint256' }, + { name: 'lzTokenFee', type: 'uint256' }, + ], + }, + ], + }, + { + name: 'send', + type: 'function', + stateMutability: 'payable', + inputs: [ + SendParamTuple, + { + type: 'tuple', + name: '_fee', + components: [ + { name: 'nativeFee', type: 'uint256' }, + { name: 'lzTokenFee', type: 'uint256' }, + ], + }, + { name: '_refundAddress', type: 'address' }, + ], + outputs: [ + { + type: 'tuple', + name: 'msgReceipt', + components: [ + { name: 'guid', type: 'bytes32' }, + { name: 'nonce', type: 'uint64' }, + { + type: 'tuple', + name: 'fee', + components: [ + { name: 'nativeFee', type: 'uint256' }, + { name: 'lzTokenFee', type: 'uint256' }, + ], + }, + ], + }, + { + type: 'tuple', + name: 'oftReceipt', + components: [ + { name: 'amountSentLD', type: 'uint256' }, + { name: 'amountReceivedLD', type: 'uint256' }, + ], + }, + ], + }, +] as const satisfies Abi + +type SendParamArgs = { + dstEid: number + to: Hex + amountLD: bigint + minAmountLD: bigint + extraOptions: Hex + composeMsg: Hex + oftCmd: Hex +} + +const toSendParamArgs = (param: StargateSendParam): SendParamArgs => ({ + dstEid: param.dstEid, + to: param.to, + amountLD: param.amountLD, + minAmountLD: param.minAmountLD, + extraOptions: param.extraOptions, + composeMsg: param.composeMsg, + oftCmd: param.oftCmd, +}) + +export const encodeQuoteOFT = (sendParam: StargateSendParam): Hex => + encodeFunctionData({ + abi: IStargateAbi, + functionName: 'quoteOFT', + args: [toSendParamArgs(sendParam)], + }) + +export const decodeQuoteOFTResult = (data: Hex) => + decodeFunctionResult({ + abi: IStargateAbi, + functionName: 'quoteOFT', + data, + }) + +export const encodeQuoteSend = (sendParam: StargateSendParam, payInLzToken: boolean): Hex => + encodeFunctionData({ + abi: IStargateAbi, + functionName: 'quoteSend', + args: [toSendParamArgs(sendParam), payInLzToken], + }) + +export const decodeQuoteSendResult = (data: Hex) => + decodeFunctionResult({ + abi: IStargateAbi, + functionName: 'quoteSend', + data, + }) + +export const encodeSend = ( + sendParam: StargateSendParam, + fee: { nativeFee: bigint; lzTokenFee: bigint }, + refundAddress: Hex, +): Hex => + encodeFunctionData({ + abi: IStargateAbi, + functionName: 'send', + args: [toSendParamArgs(sendParam), fee, refundAddress], + }) diff --git a/packages/swapper/src/swappers/StargateSwapper/utils/stargateService.ts b/packages/swapper/src/swappers/StargateSwapper/utils/stargateService.ts new file mode 100644 index 00000000000..341cff31e59 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/utils/stargateService.ts @@ -0,0 +1,15 @@ +import axios from 'axios' + +import { makeSwapperAxiosServiceMonadic } from '../../../utils' + +const axiosConfig = { + timeout: 10000, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, +} + +const stargateServiceBase = axios.create(axiosConfig) + +export const stargateService = makeSwapperAxiosServiceMonadic(stargateServiceBase) diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index dbca4b28c35..0261beed837 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -47,6 +47,7 @@ import type { AcrossTransactionMetadata } from './swappers/AcrossSwapper/utils/t import type { CowMessageToSign } from './swappers/CowSwapper/types' import type { DebridgeTransactionMetadata } from './swappers/DebridgeSwapper/utils/types' import type { RelayTransactionMetadata } from './swappers/RelaySwapper/utils/types' +import type { StargateTransactionMetadata } from './swappers/StargateSwapper/types' import type { makeSwapperAxiosServiceMonadic } from './utils' // TODO: Rename all properties in this type to be camel case and not react specific @@ -85,6 +86,7 @@ export type SwapperConfig = { VITE_ACROSS_API_URL: string VITE_ACROSS_INTEGRATOR_ID: string VITE_DEBRIDGE_API_URL: string + VITE_ODOS_API_URL: string } export enum SwapperName { @@ -100,12 +102,14 @@ export enum SwapperName { ButterSwap = 'ButterSwap', Bebop = 'Bebop', NearIntents = 'NEAR Intents', + Odos = 'Odos', Cetus = 'Cetus', Sunio = 'Sun.io', Avnu = 'AVNU', Stonfi = 'STON.fi', Across = 'Across', Debridge = 'deBridge', + Stargate = 'Stargate', } export type SwapSource = SwapperName | `${SwapperName} • ${string}` @@ -510,6 +514,13 @@ export type TradeQuoteStep = { } acrossTransactionMetadata?: AcrossTransactionMetadata debridgeTransactionMetadata?: DebridgeTransactionMetadata + stargateTransactionMetadata?: StargateTransactionMetadata + odosTransactionMetadata?: { + to: string + data: string + value: string + gas: string + } affiliateFee?: AffiliateFee } @@ -1010,4 +1021,4 @@ export type MonadicSwapperAxiosService = ReturnType -} +} \ No newline at end of file diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/odos-icon.png b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/odos-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c657ab22ab2c446b9fbe08bd307b08c4ecd2d483 GIT binary patch literal 42072 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 literal 0 HcmV?d00001 diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/stargate-icon.png b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/stargate-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c48e306ed4dedfa274f0c3213fc13c39df6a670b GIT binary patch literal 7085 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 Tu4ey#+$~po}WJn literal 0 HcmV?d00001 diff --git a/src/config.ts b/src/config.ts index 662cb529474..45cf2f200f0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -279,6 +279,9 @@ const validators = { VITE_ACROSS_INTEGRATOR_ID: str({ default: '' }), VITE_FEATURE_DEBRIDGE_SWAP: bool({ default: false }), VITE_DEBRIDGE_API_URL: url({ default: 'https://dln.debridge.finance/v1.0' }), + VITE_FEATURE_ODOS_SWAP: bool({ default: false }), + VITE_ODOS_API_URL: url({ default: 'https://api.odos.xyz' }), + VITE_FEATURE_STARGATE_SWAP: bool({ default: false }), VITE_FEATURE_TX_HISTORY_BYE_BYE: bool({ default: false }), VITE_AFFILIATE_REVENUE_URL: url(), VITE_FEATURE_LEDGER_READ_ONLY: bool({ default: false }), @@ -321,4 +324,4 @@ export const getConfig = memoize(() => { return Object.freeze({ ...cleanEnv(import.meta.env ?? process.env, validators, { reporter }), }) -}) +}) \ No newline at end of file diff --git a/src/state/helpers.ts b/src/state/helpers.ts index ab979b91d28..be22d4f13c4 100644 --- a/src/state/helpers.ts +++ b/src/state/helpers.ts @@ -21,6 +21,7 @@ export const isCrossAccountTradeSupported = (swapperName: SwapperName) => { case SwapperName.Sunio: case SwapperName.Across: case SwapperName.Debridge: + case SwapperName.Stargate: return true case SwapperName.Zrx: case SwapperName.CowSwap: @@ -29,6 +30,7 @@ export const isCrossAccountTradeSupported = (swapperName: SwapperName) => { case SwapperName.Test: case SwapperName.Avnu: case SwapperName.Cetus: + case SwapperName.Odos: // Technically supported for Arbitrum Bridge, but we disable it for the sake of simplicity for now return false default: @@ -55,6 +57,8 @@ export const getEnabledSwappers = ( StonfiSwap, AcrossSwap, DebridgeSwap, + OdosSwap, + StargateSwap, }: FeatureFlags, isCrossAccountTrade: boolean, walletName?: string, @@ -112,6 +116,11 @@ export const getEnabledSwappers = ( AcrossSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Across)), [SwapperName.Debridge]: DebridgeSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Debridge)), + [SwapperName.Odos]: + OdosSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Odos)), + [SwapperName.Stargate]: + StargateSwap && + (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Stargate)), [SwapperName.Test]: false, } -} +} \ No newline at end of file diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index 898fad5e6f8..ee12cc6d779 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -126,6 +126,8 @@ export type FeatureFlags = { StonfiSwap: boolean AcrossSwap: boolean DebridgeSwap: boolean + OdosSwap: boolean + StargateSwap: boolean LazyTxHistory: boolean LedgerReadOnly: boolean QuickBuy: boolean @@ -299,6 +301,8 @@ const initialState: Preferences = { StonfiSwap: getConfig().VITE_FEATURE_STONFI_SWAP, AcrossSwap: getConfig().VITE_FEATURE_ACROSS_SWAP, DebridgeSwap: getConfig().VITE_FEATURE_DEBRIDGE_SWAP, + OdosSwap: getConfig().VITE_FEATURE_ODOS_SWAP, + StargateSwap: getConfig().VITE_FEATURE_STARGATE_SWAP, LazyTxHistory: getConfig().VITE_FEATURE_TX_HISTORY_BYE_BYE, LedgerReadOnly: getConfig().VITE_FEATURE_LEDGER_READ_ONLY, QuickBuy: getConfig().VITE_FEATURE_QUICK_BUY, @@ -472,4 +476,4 @@ export const preferences = createSlice({ selectShowTopAssetsCarousel: state => state.showTopAssetsCarousel, selectQuickBuyAmounts: state => state.quickBuyAmounts, }, -}) +}) \ No newline at end of file diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 8ee38716dcf..1d9396a53fe 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -198,6 +198,8 @@ export const mockStore: ReduxState = { StonfiSwap: false, AcrossSwap: false, DebridgeSwap: false, + OdosSwap: false, + StargateSwap: false, LazyTxHistory: false, QuickBuy: false, NewWalletManager: false, @@ -461,4 +463,4 @@ export const mockStore: ReduxState = { isChatHistoryOpen: false, messagesByConversation: {}, }, -} +} \ No newline at end of file From 1c8e99a1a420bd57448a3f0546958b9dae05eb96 Mon Sep 17 00:00:00 2001 From: Discostu Date: Tue, 7 Apr 2026 11:20:37 +0200 Subject: [PATCH 02/17] fix: stargate swapper rule compliance - Add VITE_FEATURE_STARGATE_SWAP to .env.development and .env.production - Remove unused CSP domain from Stargate.ts - Fix unsafe double cast in fetchStargateTrade --- .env.development | 1 + .env.production | 1 + headers/csps/defi/swappers/Stargate.ts | 7 ++----- .../StargateSwapper/utils/fetchStargateTrade.ts | 10 ++++++---- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.env.development b/.env.development index 48d2f40d91b..3efb00a6079 100644 --- a/.env.development +++ b/.env.development @@ -112,3 +112,4 @@ VITE_PROXY_API_BASE_URL=https://dev-api.proxy.shapeshift.com # Agentic Chat # VITE_AGENTIC_SERVER_BASE_URL=http://localhost:4111 +VITE_FEATURE_STARGATE_SWAP=true diff --git a/.env.production b/.env.production index a8162c25ac2..b71c066a747 100644 --- a/.env.production +++ b/.env.production @@ -9,3 +9,4 @@ VITE_FEATURE_CELO=false # mixpanel VITE_MIXPANEL_TOKEN=9d304465fc72224aead9e027e7c24356 +VITE_FEATURE_STARGATE_SWAP=false diff --git a/headers/csps/defi/swappers/Stargate.ts b/headers/csps/defi/swappers/Stargate.ts index c76f9eab4ff..8c93443f555 100644 --- a/headers/csps/defi/swappers/Stargate.ts +++ b/headers/csps/defi/swappers/Stargate.ts @@ -1,8 +1,5 @@ import type { Csp } from '../../../types' export const csp: Csp = { - 'connect-src': [ - 'https://mainnet.stargate-api.com', - 'https://scan.layerzero-api.com', - ], -} + 'connect-src': ['https://scan.layerzero-api.com'], +} \ No newline at end of file diff --git a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts index c2b2f491fe7..e1cd9094017 100644 --- a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts +++ b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts @@ -230,9 +230,11 @@ export async function fetchStargateTrade({ ) } - const messagingFee: StargateMessagingFee = decodeQuoteSendResult( - quoteSendResult.data as Hex, - ) as unknown as StargateMessagingFee + const rawMessagingFee = decodeQuoteSendResult(quoteSendResult.data as Hex) + const messagingFee: StargateMessagingFee = { + nativeFee: (rawMessagingFee as { nativeFee: bigint; lzTokenFee: bigint }).nativeFee, + lzTokenFee: (rawMessagingFee as { nativeFee: bigint; lzTokenFee: bigint }).lzTokenFee, + } const buyAmountAfterFeesCryptoBaseUnit = detailDstAmountLD.toString() const buyAmountBeforeFeesCryptoBaseUnit = (detailDstAmountLD + detailFeeAmountLD).toString() @@ -360,4 +362,4 @@ export async function fetchStargateTrade({ }), ) } -} +} \ No newline at end of file From 28ddec2e01a198d899a2cdcba1de3c0738d1933a Mon Sep 17 00:00:00 2001 From: Discostu Date: Tue, 7 Apr 2026 11:43:19 +0200 Subject: [PATCH 03/17] fix: use correct LayerZero scan API endpoint Replace dead scan.layerzero-api.com with working api-mainnet.layerzero-scan.com for status tracking. --- headers/csps/defi/swappers/Stargate.ts | 2 +- packages/swapper/src/swappers/StargateSwapper/endpoints.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/headers/csps/defi/swappers/Stargate.ts b/headers/csps/defi/swappers/Stargate.ts index 8c93443f555..ba955887710 100644 --- a/headers/csps/defi/swappers/Stargate.ts +++ b/headers/csps/defi/swappers/Stargate.ts @@ -1,5 +1,5 @@ import type { Csp } from '../../../types' export const csp: Csp = { - 'connect-src': ['https://scan.layerzero-api.com'], + 'connect-src': ['https://api-mainnet.layerzero-scan.com'], } \ No newline at end of file diff --git a/packages/swapper/src/swappers/StargateSwapper/endpoints.ts b/packages/swapper/src/swappers/StargateSwapper/endpoints.ts index ab54e5a586d..13e09536935 100644 --- a/packages/swapper/src/swappers/StargateSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/StargateSwapper/endpoints.ts @@ -96,7 +96,7 @@ export const stargateApi: SwapperApi = { } const maybeStatusResponse = await stargateService.get( - `https://scan.layerzero-api.com/v1/messages/tx/${txHash}`, + `https://api-mainnet.layerzero-scan.com/tx/${txHash}`, ) if (maybeStatusResponse.isErr()) { @@ -146,4 +146,4 @@ export const stargateApi: SwapperApi = { message, } }, -} +} \ No newline at end of file From 054c2771366457bcf44f9ce33eef04c7fc57ba90 Mon Sep 17 00:00:00 2001 From: Discostu Date: Tue, 7 Apr 2026 19:08:49 +0200 Subject: [PATCH 04/17] fix: add missing newline at end of fetchStargateTrade.ts --- .../src/swappers/StargateSwapper/utils/fetchStargateTrade.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts index e1cd9094017..140ced8dd18 100644 --- a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts +++ b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts @@ -362,4 +362,4 @@ export async function fetchStargateTrade({ }), ) } -} \ No newline at end of file +} From 447f81e0a09ba7f5b824165f978f2d8cd340ec6c Mon Sep 17 00:00:00 2001 From: Discostu Date: Tue, 7 Apr 2026 19:15:41 +0200 Subject: [PATCH 05/17] fix: prettier newline in StargateSwapper endpoints --- packages/swapper/src/swappers/StargateSwapper/endpoints.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/swapper/src/swappers/StargateSwapper/endpoints.ts b/packages/swapper/src/swappers/StargateSwapper/endpoints.ts index 13e09536935..d7a3c1aab71 100644 --- a/packages/swapper/src/swappers/StargateSwapper/endpoints.ts +++ b/packages/swapper/src/swappers/StargateSwapper/endpoints.ts @@ -146,4 +146,4 @@ export const stargateApi: SwapperApi = { message, } }, -} \ No newline at end of file +} From 3bf2c82219fb259fc4c7ee1dbca902c7ac5e7a63 Mon Sep 17 00:00:00 2001 From: Discostu Date: Wed, 8 Apr 2026 11:50:11 +0200 Subject: [PATCH 06/17] chore: remove Odos scaffolding from Stargate branch Odos integration is incomplete and unrelated to Stargate. Moving all Odos references to a dedicated feat/odos-swapper branch. Co-Authored-By: Claude Sonnet 4.6 --- .env | 4 ---- headers/csps/defi/swappers/Odos.ts | 5 ----- headers/csps/index.ts | 2 -- packages/swapper/src/constants.ts | 9 --------- packages/swapper/src/types.ts | 8 -------- .../components/SwapperIcon/SwapperIcon.tsx | 3 --- .../components/SwapperIcon/odos-icon.png | Bin 42072 -> 0 bytes src/config.ts | 2 -- src/state/helpers.ts | 4 ---- .../slices/preferencesSlice/preferencesSlice.ts | 2 -- src/test/mocks/store.ts | 1 - 11 files changed, 40 deletions(-) delete mode 100644 headers/csps/defi/swappers/Odos.ts delete mode 100644 src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/odos-icon.png diff --git a/.env b/.env index 112cfc41381..e81c211fc6e 100644 --- a/.env +++ b/.env @@ -116,7 +116,6 @@ VITE_FEATURE_TON=true VITE_FEATURE_EARN_TAB=true VITE_FEATURE_ACROSS_SWAP=true VITE_FEATURE_DEBRIDGE_SWAP=true -VITE_FEATURE_ODOS_SWAP=false VITE_FEATURE_STARGATE_SWAP=false VITE_FEATURE_USERBACK=true VITE_FEATURE_AGENTIC_CHAT=false @@ -362,9 +361,6 @@ VITE_ACROSS_INTEGRATOR_ID= # deBridge DLN VITE_DEBRIDGE_API_URL=https://dln.debridge.finance/v1.0 -# Odos -VITE_ODOS_API_URL=https://api.odos.xyz - # Userback feedback widget VITE_USERBACK_TOKEN=A-3gHopRTd55QqxXGsJd0XLVVG3 diff --git a/headers/csps/defi/swappers/Odos.ts b/headers/csps/defi/swappers/Odos.ts deleted file mode 100644 index 27dfc347b8f..00000000000 --- a/headers/csps/defi/swappers/Odos.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Csp } from '../../../types' - -export const csp: Csp = { - 'connect-src': ['https://api.odos.xyz'], -} diff --git a/headers/csps/index.ts b/headers/csps/index.ts index e5973cb9963..29a9085140a 100644 --- a/headers/csps/index.ts +++ b/headers/csps/index.ts @@ -65,7 +65,6 @@ import { csp as bebop } from './defi/swappers/Bebop' import { csp as butterSwap } from './defi/swappers/ButterSwap' import { csp as cowSwap } from './defi/swappers/CowSwap' import { csp as nearIntents } from './defi/swappers/NearIntents' -import { csp as odos } from './defi/swappers/Odos' import { csp as oneInch } from './defi/swappers/OneInch' import { csp as stargate } from './defi/swappers/Stargate' import { csp as portals } from './defi/swappers/Portals' @@ -189,7 +188,6 @@ export const csps = [ bebop, cowSwap, nearIntents, - odos, oneInch, portals, stonfi, diff --git a/packages/swapper/src/constants.ts b/packages/swapper/src/constants.ts index 3554f670596..d04f8f27ec2 100644 --- a/packages/swapper/src/constants.ts +++ b/packages/swapper/src/constants.ts @@ -22,8 +22,6 @@ import { mayachainApi } from './swappers/MayachainSwapper/endpoints' import { mayachainSwapper } from './swappers/MayachainSwapper/MayachainSwapper' import { nearIntentsApi } from './swappers/NearIntentsSwapper/endpoints' import { nearIntentsSwapper } from './swappers/NearIntentsSwapper/NearIntentsSwapper' -import { odosApi } from './swappers/OdosSwapper/endpoints' -import { odosSwapper } from './swappers/OdosSwapper/OdosSwapper' import { stargateApi } from './swappers/StargateSwapper/endpoints' import { stargateSwapper } from './swappers/StargateSwapper/StargateSwapper' import { portalsApi } from './swappers/PortalsSwapper/endpoints' @@ -120,10 +118,6 @@ export const swappers: Record = ...debridgeSwapper, ...debridgeApi, }, - [SwapperName.Odos]: { - ...odosSwapper, - ...odosApi, - }, [SwapperName.Stargate]: { ...stargateSwapper, ...stargateApi, @@ -180,8 +174,6 @@ export const getDefaultSlippageDecimalPercentageForSwapper = ( return DEFAULT_BUTTERSWAP_SLIPPAGE_DECIMAL_PERCENTAGE case SwapperName.NearIntents: return DEFAULT_NEAR_INTENTS_SLIPPAGE_DECIMAL_PERCENTAGE - case SwapperName.Odos: - return '0.003' case SwapperName.Cetus: return DEFAULT_CETUS_SLIPPAGE_DECIMAL_PERCENTAGE case SwapperName.Sunio: @@ -203,7 +195,6 @@ export const isAutoSlippageSupportedBySwapper = (swapperName: SwapperName): bool case SwapperName.Across: case SwapperName.Debridge: return true - case SwapperName.Odos: case SwapperName.Stargate: return false default: diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index 0261beed837..ed68c25698a 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -86,7 +86,6 @@ export type SwapperConfig = { VITE_ACROSS_API_URL: string VITE_ACROSS_INTEGRATOR_ID: string VITE_DEBRIDGE_API_URL: string - VITE_ODOS_API_URL: string } export enum SwapperName { @@ -102,7 +101,6 @@ export enum SwapperName { ButterSwap = 'ButterSwap', Bebop = 'Bebop', NearIntents = 'NEAR Intents', - Odos = 'Odos', Cetus = 'Cetus', Sunio = 'Sun.io', Avnu = 'AVNU', @@ -515,12 +513,6 @@ export type TradeQuoteStep = { acrossTransactionMetadata?: AcrossTransactionMetadata debridgeTransactionMetadata?: DebridgeTransactionMetadata stargateTransactionMetadata?: StargateTransactionMetadata - odosTransactionMetadata?: { - to: string - data: string - value: string - gas: string - } affiliateFee?: AffiliateFee } diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx index a58068e4910..8b3b3f1c4f6 100644 --- a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx +++ b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/SwapperIcon.tsx @@ -14,7 +14,6 @@ import CowIcon from './cow-icon.png' import DebridgeIcon from './debridge-icon.svg' import MayachainIcon from './maya_logo.png' import NearIntentsIcon from './near-intents-icon.png' -import OdosIcon from './odos-icon.png' import PortalsIcon from './portals-icon.png' import RelayIcon from './relay-icon.svg' import StargateIcon from './stargate-icon.png' @@ -68,8 +67,6 @@ export const SwapperIcon = ({ return AcrossIcon case SwapperName.Debridge: return DebridgeIcon - case SwapperName.Odos: - return OdosIcon case SwapperName.Stargate: return StargateIcon case SwapperName.Test: diff --git a/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/odos-icon.png b/src/components/MultiHopTrade/components/TradeInput/components/SwapperIcon/odos-icon.png deleted file mode 100644 index c657ab22ab2c446b9fbe08bd307b08c4ecd2d483..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42072 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 diff --git a/src/config.ts b/src/config.ts index 45cf2f200f0..b1f5c323ac6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -279,8 +279,6 @@ const validators = { VITE_ACROSS_INTEGRATOR_ID: str({ default: '' }), VITE_FEATURE_DEBRIDGE_SWAP: bool({ default: false }), VITE_DEBRIDGE_API_URL: url({ default: 'https://dln.debridge.finance/v1.0' }), - VITE_FEATURE_ODOS_SWAP: bool({ default: false }), - VITE_ODOS_API_URL: url({ default: 'https://api.odos.xyz' }), VITE_FEATURE_STARGATE_SWAP: bool({ default: false }), VITE_FEATURE_TX_HISTORY_BYE_BYE: bool({ default: false }), VITE_AFFILIATE_REVENUE_URL: url(), diff --git a/src/state/helpers.ts b/src/state/helpers.ts index be22d4f13c4..e47f7f47007 100644 --- a/src/state/helpers.ts +++ b/src/state/helpers.ts @@ -30,7 +30,6 @@ export const isCrossAccountTradeSupported = (swapperName: SwapperName) => { case SwapperName.Test: case SwapperName.Avnu: case SwapperName.Cetus: - case SwapperName.Odos: // Technically supported for Arbitrum Bridge, but we disable it for the sake of simplicity for now return false default: @@ -57,7 +56,6 @@ export const getEnabledSwappers = ( StonfiSwap, AcrossSwap, DebridgeSwap, - OdosSwap, StargateSwap, }: FeatureFlags, isCrossAccountTrade: boolean, @@ -116,8 +114,6 @@ export const getEnabledSwappers = ( AcrossSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Across)), [SwapperName.Debridge]: DebridgeSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Debridge)), - [SwapperName.Odos]: - OdosSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Odos)), [SwapperName.Stargate]: StargateSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Stargate)), diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index ee12cc6d779..79c5ac8995d 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -126,7 +126,6 @@ export type FeatureFlags = { StonfiSwap: boolean AcrossSwap: boolean DebridgeSwap: boolean - OdosSwap: boolean StargateSwap: boolean LazyTxHistory: boolean LedgerReadOnly: boolean @@ -301,7 +300,6 @@ const initialState: Preferences = { StonfiSwap: getConfig().VITE_FEATURE_STONFI_SWAP, AcrossSwap: getConfig().VITE_FEATURE_ACROSS_SWAP, DebridgeSwap: getConfig().VITE_FEATURE_DEBRIDGE_SWAP, - OdosSwap: getConfig().VITE_FEATURE_ODOS_SWAP, StargateSwap: getConfig().VITE_FEATURE_STARGATE_SWAP, LazyTxHistory: getConfig().VITE_FEATURE_TX_HISTORY_BYE_BYE, LedgerReadOnly: getConfig().VITE_FEATURE_LEDGER_READ_ONLY, diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 1d9396a53fe..5852bf84be5 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -198,7 +198,6 @@ export const mockStore: ReduxState = { StonfiSwap: false, AcrossSwap: false, DebridgeSwap: false, - OdosSwap: false, StargateSwap: false, LazyTxHistory: false, QuickBuy: false, From 6237fe180bdf0a29258b62562321bb29bfe47db5 Mon Sep 17 00:00:00 2001 From: Discostu Date: Wed, 8 Apr 2026 14:27:18 +0200 Subject: [PATCH 07/17] chore: apply lint fixes to Stargate swapper integration Co-Authored-By: Claude Sonnet 4.6 --- headers/csps/defi/swappers/Stargate.ts | 2 +- headers/csps/index.ts | 4 ++-- packages/swapper/src/constants.ts | 6 +++--- packages/swapper/src/types.ts | 2 +- .../TradeInput/components/SwapperIcon/SwapperIcon.tsx | 2 +- src/config.ts | 2 +- src/state/helpers.ts | 5 ++--- src/state/slices/preferencesSlice/preferencesSlice.ts | 2 +- src/test/mocks/store.ts | 2 +- 9 files changed, 13 insertions(+), 14 deletions(-) diff --git a/headers/csps/defi/swappers/Stargate.ts b/headers/csps/defi/swappers/Stargate.ts index ba955887710..8df257ecf64 100644 --- a/headers/csps/defi/swappers/Stargate.ts +++ b/headers/csps/defi/swappers/Stargate.ts @@ -2,4 +2,4 @@ import type { Csp } from '../../../types' export const csp: Csp = { 'connect-src': ['https://api-mainnet.layerzero-scan.com'], -} \ No newline at end of file +} diff --git a/headers/csps/index.ts b/headers/csps/index.ts index 29a9085140a..a01c8e569d1 100644 --- a/headers/csps/index.ts +++ b/headers/csps/index.ts @@ -66,8 +66,8 @@ import { csp as butterSwap } from './defi/swappers/ButterSwap' import { csp as cowSwap } from './defi/swappers/CowSwap' import { csp as nearIntents } from './defi/swappers/NearIntents' import { csp as oneInch } from './defi/swappers/OneInch' -import { csp as stargate } from './defi/swappers/Stargate' import { csp as portals } from './defi/swappers/Portals' +import { csp as stargate } from './defi/swappers/Stargate' import { csp as stonfi } from './defi/swappers/Stonfi' import { csp as sunio } from './defi/swappers/Sunio' import { csp as thor } from './defi/swappers/Thor' @@ -208,4 +208,4 @@ export const csps = [ railway, discord, yieldxyz, -] \ No newline at end of file +] diff --git a/packages/swapper/src/constants.ts b/packages/swapper/src/constants.ts index d04f8f27ec2..6084787e855 100644 --- a/packages/swapper/src/constants.ts +++ b/packages/swapper/src/constants.ts @@ -22,12 +22,12 @@ import { mayachainApi } from './swappers/MayachainSwapper/endpoints' import { mayachainSwapper } from './swappers/MayachainSwapper/MayachainSwapper' import { nearIntentsApi } from './swappers/NearIntentsSwapper/endpoints' import { nearIntentsSwapper } from './swappers/NearIntentsSwapper/NearIntentsSwapper' -import { stargateApi } from './swappers/StargateSwapper/endpoints' -import { stargateSwapper } from './swappers/StargateSwapper/StargateSwapper' import { portalsApi } from './swappers/PortalsSwapper/endpoints' import { portalsSwapper } from './swappers/PortalsSwapper/PortalsSwapper' import { relaySwapper } from './swappers/RelaySwapper' import { relayApi } from './swappers/RelaySwapper/endpoints' +import { stargateApi } from './swappers/StargateSwapper/endpoints' +import { stargateSwapper } from './swappers/StargateSwapper/StargateSwapper' import { stonfiApi } from './swappers/StonfiSwapper/endpoints' import { stonfiSwapper } from './swappers/StonfiSwapper/StonfiSwapper' import { sunioApi } from './swappers/SunioSwapper/endpoints' @@ -200,4 +200,4 @@ export const isAutoSlippageSupportedBySwapper = (swapperName: SwapperName): bool default: return false } -} \ No newline at end of file +} diff --git a/packages/swapper/src/types.ts b/packages/swapper/src/types.ts index ed68c25698a..16d070df1f0 100644 --- a/packages/swapper/src/types.ts +++ b/packages/swapper/src/types.ts @@ -1013,4 +1013,4 @@ export type MonadicSwapperAxiosService = ReturnType -} \ No newline at end of file +} diff --git a/src/config.ts b/src/config.ts index b1f5c323ac6..7cf1d7798f6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -322,4 +322,4 @@ export const getConfig = memoize(() => { return Object.freeze({ ...cleanEnv(import.meta.env ?? process.env, validators, { reporter }), }) -}) \ No newline at end of file +}) diff --git a/src/state/helpers.ts b/src/state/helpers.ts index e47f7f47007..07dff41f36f 100644 --- a/src/state/helpers.ts +++ b/src/state/helpers.ts @@ -115,8 +115,7 @@ export const getEnabledSwappers = ( [SwapperName.Debridge]: DebridgeSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Debridge)), [SwapperName.Stargate]: - StargateSwap && - (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Stargate)), + StargateSwap && (!isCrossAccountTrade || isCrossAccountTradeSupported(SwapperName.Stargate)), [SwapperName.Test]: false, } -} \ No newline at end of file +} diff --git a/src/state/slices/preferencesSlice/preferencesSlice.ts b/src/state/slices/preferencesSlice/preferencesSlice.ts index 79c5ac8995d..7d17d78c65d 100644 --- a/src/state/slices/preferencesSlice/preferencesSlice.ts +++ b/src/state/slices/preferencesSlice/preferencesSlice.ts @@ -474,4 +474,4 @@ export const preferences = createSlice({ selectShowTopAssetsCarousel: state => state.showTopAssetsCarousel, selectQuickBuyAmounts: state => state.quickBuyAmounts, }, -}) \ No newline at end of file +}) diff --git a/src/test/mocks/store.ts b/src/test/mocks/store.ts index 5852bf84be5..5ae2a03337e 100644 --- a/src/test/mocks/store.ts +++ b/src/test/mocks/store.ts @@ -462,4 +462,4 @@ export const mockStore: ReduxState = { isChatHistoryOpen: false, messagesByConversation: {}, }, -} \ No newline at end of file +} From aa6db85686cd53ffb5f575c0a004216f92c44c73 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 12 Apr 2026 14:44:15 +0200 Subject: [PATCH 08/17] fix(stargate): apply slippage to minAmountLD, add TradeQuoteError codes, add tests - Apply slippage to sendParam.minAmountLD before encodeSend() so on-chain fills worse than quote+slippage will revert (was hardcoded 0n, ignoring slippage) - Add TradeQuoteError.InternalError codes to sendAddress/receiveAddress validation errors in getTradeQuote so callers can distinguish input errors from route failures - Move VITE_FEATURE_STARGATE_SWAP into the feature-flags section in .env.production - Add getTradeQuote.test.ts with 9 test cases covering happy path, error cases, slippage application, fee structure, and step metadata Co-Authored-By: Claude Sonnet 4.6 --- .env.production | 2 +- .../getTradeQuote/getTradeQuote.test.ts | 167 ++++++++++++++++++ .../getTradeQuote/getTradeQuote.ts | 5 + .../utils/fetchStargateTrade.ts | 8 + 4 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts diff --git a/.env.production b/.env.production index b71c066a747..97307880122 100644 --- a/.env.production +++ b/.env.production @@ -6,7 +6,7 @@ VITE_FEATURE_THORCHAIN_TCY_ACTIVITY=false VITE_FEATURE_AGENTIC_CHAT=false VITE_FEATURE_FLOWEVM=false VITE_FEATURE_CELO=false +VITE_FEATURE_STARGATE_SWAP=false # mixpanel VITE_MIXPANEL_TOKEN=9d304465fc72224aead9e027e7c24356 -VITE_FEATURE_STARGATE_SWAP=false diff --git a/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts new file mode 100644 index 00000000000..a19bebf2bc5 --- /dev/null +++ b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts @@ -0,0 +1,167 @@ +import { arbitrumChainId, ethChainId } from '@shapeshiftoss/caip' +import type { EvmChainAdapter } from '@shapeshiftoss/chain-adapters' +import { describe, expect, it, vi } from 'vitest' + +import type { GetEvmTradeQuoteInputBase, SwapperDeps } from '../../../types' +import { SwapperName, TradeQuoteError } from '../../../types' +import { ETH, USDC_ARBITRUM, USDC_MAINNET } from '../../utils/test-data/assets' +import { getTradeQuote } from './getTradeQuote' + +vi.mock('@shapeshiftoss/contracts', () => ({ + viemClientByChainId: { + [ethChainId]: { + call: vi.fn().mockResolvedValue({ data: '0xdeadbeef' }), + }, + }, +})) + +vi.mock('@shapeshiftoss/chain-adapters', async () => { + const actual = await vi.importActual('@shapeshiftoss/chain-adapters') + return { + ...actual, + evm: { + getFees: vi.fn().mockResolvedValue({ networkFeeCryptoBaseUnit: '21000' }), + calcNetworkFeeCryptoBaseUnit: vi.fn().mockReturnValue('21000'), + }, + } +}) + +vi.mock('../utils/helpers', () => ({ + encodeQuoteOFT: vi.fn().mockReturnValue('0x11'), + decodeQuoteOFTResult: vi.fn().mockReturnValue([ + {}, + [], + { amountReceivedLD: 990_000_000n, amountSentLD: 1_000_000_000n }, + ]), + encodeQuoteSend: vi.fn().mockReturnValue('0x22'), + decodeQuoteSendResult: vi.fn().mockReturnValue({ + nativeFee: 1_000_000_000_000_000n, + lzTokenFee: 0n, + }), + encodeSend: vi.fn().mockReturnValue('0x33'), +})) + +describe('Stargate getTradeQuote', () => { + const mockAdapter = { + getGasFeeData: vi.fn().mockResolvedValue({ + average: { gasPrice: '42', maxFeePerGas: '42' }, + }), + } as unknown as EvmChainAdapter + + const deps = { + assertGetEvmChainAdapter: () => mockAdapter, + } as unknown as SwapperDeps + + const commonInput = { + sellAsset: USDC_MAINNET, + buyAsset: USDC_ARBITRUM, + accountNumber: 0, + affiliateBps: '0', + sellAmountIncludingProtocolFeesCryptoBaseUnit: '1000000000', + sendAddress: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + receiveAddress: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', + slippageTolerancePercentageDecimal: '0.005', + } as unknown as GetEvmTradeQuoteInputBase + + it('returns error when sendAddress is missing', async () => { + const result = await getTradeQuote({ ...commonInput, sendAddress: undefined }, deps) + expect(result.isErr()).toBe(true) + const err = result.unwrapErr() + expect(err.message).toBe('sendAddress is required') + expect(err.code).toBe(TradeQuoteError.InternalError) + }) + + it('returns error when receiveAddress is missing', async () => { + const result = await getTradeQuote({ ...commonInput, receiveAddress: undefined }, deps) + expect(result.isErr()).toBe(true) + const err = result.unwrapErr() + expect(err.message).toBe('receiveAddress is required') + expect(err.code).toBe(TradeQuoteError.InternalError) + }) + + it('returns UnsupportedTradePair error for same-chain swap', async () => { + const result = await getTradeQuote( + { ...commonInput, buyAsset: USDC_MAINNET }, + deps, + ) + expect(result.isErr()).toBe(true) + expect(result.unwrapErr().code).toBe(TradeQuoteError.UnsupportedTradePair) + }) + + it('returns UnsupportedChain error for unsupported sell chain', async () => { + // ETH on mainnet → USDC Arbitrum but sell asset on a non-Stargate chain + const result = await getTradeQuote( + { + ...commonInput, + sellAsset: { ...ETH, chainId: 'eip155:5' as const }, // Goerli (not supported) + }, + deps, + ) + expect(result.isErr()).toBe(true) + expect(result.unwrapErr().code).toBe(TradeQuoteError.UnsupportedChain) + }) + + it('returns a valid quote for USDC Ethereum → USDC Arbitrum', async () => { + const result = await getTradeQuote(commonInput, deps) + expect(result.isOk()).toBe(true) + + const quotes = result.unwrap() + expect(quotes).toHaveLength(1) + + const quote = quotes[0] + expect(quote.swapperName).toBe(SwapperName.Stargate) + expect(quote.receiveAddress).toBe(commonInput.receiveAddress) + expect(quote.quoteOrRate).toBe('quote') + expect(quote.slippageTolerancePercentageDecimal).toBe('0.005') + }) + + it('quote step has correct source and chain metadata', async () => { + const result = await getTradeQuote(commonInput, deps) + const step = result.unwrap()[0].steps[0] + + expect(step.source).toBe(SwapperName.Stargate) + expect(step.sellAsset.chainId).toBe(ethChainId) + expect(step.buyAsset.chainId).toBe(arbitrumChainId) + expect(step.estimatedExecutionTimeMs).toBe(60_000) + }) + + it('quote step includes protocol fees and network fee', async () => { + const result = await getTradeQuote(commonInput, deps) + const step = result.unwrap()[0].steps[0] + + expect(step.feeData.networkFeeCryptoBaseUnit).toBeDefined() + expect(step.feeData.protocolFees).toBeDefined() + const protocolFee = step.feeData.protocolFees[USDC_MAINNET.assetId] + expect(protocolFee).toBeDefined() + // fee = amountSentLD - amountReceivedLD = 1_000_000_000 - 990_000_000 = 10_000_000 + expect(protocolFee.amountCryptoBaseUnit).toBe('10000000') + }) + + it('quote step has stargateTransactionMetadata', async () => { + const result = await getTradeQuote(commonInput, deps) + const step = result.unwrap()[0].steps[0] + + expect(step.stargateTransactionMetadata).toBeDefined() + expect(step.stargateTransactionMetadata.to).toBeDefined() + expect(step.stargateTransactionMetadata.data).toBe('0x33') + }) + + it('buyAmountAfterFees reflects the received amount from quoteOFT', async () => { + const result = await getTradeQuote(commonInput, deps) + const step = result.unwrap()[0].steps[0] + + // mocked amountReceivedLD = 990_000_000 + expect(step.buyAmountAfterFeesCryptoBaseUnit).toBe('990000000') + expect(step.buyAmountBeforeFeesCryptoBaseUnit).toBe('1000000000') + }) + + it('slippage is applied: minAmountLD = detailDstAmountLD * (1 - slippage)', async () => { + const { encodeSend } = await import('../utils/helpers') + + await getTradeQuote(commonInput, deps) + + // encodeSend should have been called with sendParam.minAmountLD = 990_000_000 * 0.995 = 985_050_000 + const sendParamArg = vi.mocked(encodeSend).mock.calls[0][0] + expect(sendParamArg.minAmountLD).toBe(985_050_000n) + }) +}) diff --git a/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.ts b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.ts index a88cb988190..57ef1f64d8f 100644 --- a/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.ts +++ b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.ts @@ -2,6 +2,7 @@ import type { Result } from '@sniptt/monads' import { Err } from '@sniptt/monads' import type { CommonTradeQuoteInput, SwapErrorRight, SwapperDeps, TradeQuote } from '../../../types' +import { TradeQuoteError } from '../../../types' import { makeSwapErrorRight } from '../../../utils' import { fetchStargateTrade } from '../utils/fetchStargateTrade' @@ -14,6 +15,8 @@ export const getTradeQuote = ( Err( makeSwapErrorRight({ message: 'sendAddress is required', + code: TradeQuoteError.InternalError, + details: { field: 'sendAddress' }, }), ), ) @@ -24,6 +27,8 @@ export const getTradeQuote = ( Err( makeSwapErrorRight({ message: 'receiveAddress is required', + code: TradeQuoteError.InternalError, + details: { field: 'receiveAddress' }, }), ), ) diff --git a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts index 140ced8dd18..3e655ed7eef 100644 --- a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts +++ b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts @@ -214,6 +214,14 @@ export async function fetchStargateTrade({ const detailDstAmountLD = receipt.amountReceivedLD const detailFeeAmountLD = receipt.amountSentLD - receipt.amountReceivedLD + // Apply slippage to minAmountLD so the on-chain send() will revert if the + // fill is worse than the quoted amount minus user-selected (or default) slippage. + const DEFAULT_SLIPPAGE_BPS = 50n // 0.5% + const slippageBps = input.slippageTolerancePercentageDecimal + ? BigInt(Math.round(parseFloat(input.slippageTolerancePercentageDecimal) * 10000)) + : DEFAULT_SLIPPAGE_BPS + sendParam.minAmountLD = (detailDstAmountLD * (10000n - slippageBps)) / 10000n + const quoteSendCalldata = encodeQuoteSend(sendParam, false) const quoteSendResult = await publicClient.call({ From c0c8662d7abec27301d3f38ecd52bfcbb5617321 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 12 Apr 2026 14:59:21 +0200 Subject: [PATCH 09/17] docs: document unchained-client generate prerequisite for running tests Tests fail with "generated/ethereum/runtime not found" if the OpenAPI clients haven't been generated. Document the required generate step and the correct vi.importActual pattern for mocks. Co-Authored-By: Claude Sonnet 4.6 --- AGENTS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 2141357a451..ea942890364 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,12 @@ This file is the canonical instruction entrypoint for local agent tooling in thi - Prefer `origin` for new feature/fix branch pushes when permissions allow. - If a PR branch is already on `fork`, keep using that existing `fork` branch. - Run `pnpm run lint --fix` and `pnpm run type-check` after code changes. +- **Before running tests**: generate `unchained-client` OpenAPI clients first (requires Java). Tests fail with `./generated/ethereum/runtime not found` without this step: + ```bash + JAVA_HOME=/tmp/jdk-21.0.10+7-jre/Contents/Home PATH="$JAVA_HOME/bin:$PATH" \ + pnpm --filter @shapeshiftoss/unchained-client generate + ``` +- **Test mock pattern**: always use `vi.importActual` for real packages, only override specific functions. - Use `.github/PULL_REQUEST_TEMPLATE.md` for PR bodies. ## Routing From dd769c475ded7563567a263775970bb54ebb96d6 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 12 Apr 2026 15:06:06 +0200 Subject: [PATCH 10/17] fix(stargate): simplify type cast, log fee fallback, use real gasLimit - Destructure decodeQuoteSendResult with single cast instead of double - Add console.warn in fee-estimation catch block (matches swapper pattern) - Capture gasLimit from evm.getFees and use in stargateTransactionMetadata - Move VITE_FEATURE_STARGATE_SWAP to correct alphabetical position in .env Co-Authored-By: Claude Sonnet 4.6 --- .env | 2 +- .../utils/fetchStargateTrade.ts | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.env b/.env index e81c211fc6e..260212f7038 100644 --- a/.env +++ b/.env @@ -6,6 +6,7 @@ VITE_FEATURE_POLYGON=true VITE_FEATURE_GNOSIS=true VITE_FEATURE_ARBITRUM=true VITE_FEATURE_SOLANA=true +VITE_FEATURE_STARGATE_SWAP=false VITE_FEATURE_STARKNET=true VITE_FEATURE_SUI=true VITE_FEATURE_MAYACHAIN=true @@ -116,7 +117,6 @@ VITE_FEATURE_TON=true VITE_FEATURE_EARN_TAB=true VITE_FEATURE_ACROSS_SWAP=true VITE_FEATURE_DEBRIDGE_SWAP=true -VITE_FEATURE_STARGATE_SWAP=false VITE_FEATURE_USERBACK=true VITE_FEATURE_AGENTIC_CHAT=false VITE_FEATURE_MM_NATIVE_MULTICHAIN=false diff --git a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts index 3e655ed7eef..c243490b38d 100644 --- a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts +++ b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts @@ -238,11 +238,10 @@ export async function fetchStargateTrade({ ) } - const rawMessagingFee = decodeQuoteSendResult(quoteSendResult.data as Hex) - const messagingFee: StargateMessagingFee = { - nativeFee: (rawMessagingFee as { nativeFee: bigint; lzTokenFee: bigint }).nativeFee, - lzTokenFee: (rawMessagingFee as { nativeFee: bigint; lzTokenFee: bigint }).lzTokenFee, - } + const { nativeFee, lzTokenFee } = decodeQuoteSendResult( + quoteSendResult.data as Hex, + ) as { nativeFee: bigint; lzTokenFee: bigint } + const messagingFee: StargateMessagingFee = { nativeFee, lzTokenFee } const buyAmountAfterFeesCryptoBaseUnit = detailDstAmountLD.toString() const buyAmountBeforeFeesCryptoBaseUnit = (detailDstAmountLD + detailFeeAmountLD).toString() @@ -273,6 +272,7 @@ export async function fetchStargateTrade({ const { average } = await adapter.getGasFeeData() const supportsEIP1559 = 'maxFeePerGas' in average + let gasLimit = '500000' const networkFeeCryptoBaseUnit = await (async () => { try { const feeData = await evm.getFees({ @@ -283,12 +283,17 @@ export async function fetchStargateTrade({ from: sendAddress, supportsEIP1559, }) + gasLimit = feeData.gasLimit ?? gasLimit return feeData.networkFeeCryptoBaseUnit - } catch { + } catch (e) { + console.warn('[Stargate] Fee estimation failed, using fallback gas limit', { + error: e instanceof Error ? e.message : String(e), + sellAsset: sellAsset.assetId, + }) return evm.calcNetworkFeeCryptoBaseUnit({ ...average, supportsEIP1559, - gasLimit: '500000', + gasLimit, }) } })() @@ -297,7 +302,7 @@ export async function fetchStargateTrade({ to: contractAddress, data: sendCalldata, value: txValue, - gasLimit: '500000', + gasLimit, } const protocolFees: Record< From bc92b96e1eefb26b4c4cfc861b72929efe6ad9b8 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 12 Apr 2026 15:09:59 +0200 Subject: [PATCH 11/17] =?UTF-8?q?test(stargate):=20add=20cross-chain=20pai?= =?UTF-8?q?r=20coverage=20=E2=80=94=20native=20bridge,=20reverse,=20error?= =?UTF-8?q?=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ETH (mainnet) → ETH (Arbitrum): verifies native txValue = nativeFee + sellAmount - USDC (Arbitrum) → USDC (Mainnet): verifies reverse direction with Arbitrum sell chain - Unsupported buy chain: complements existing sell-chain error test - No Stargate contract for sell asset (FOX): expects UnsupportedTradePair error - Extend viemClientByChainId mock to cover arbitrumChainId Co-Authored-By: Claude Sonnet 4.6 --- .../getTradeQuote/getTradeQuote.test.ts | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts index a19bebf2bc5..fc33632d7db 100644 --- a/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts +++ b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest' import type { GetEvmTradeQuoteInputBase, SwapperDeps } from '../../../types' import { SwapperName, TradeQuoteError } from '../../../types' -import { ETH, USDC_ARBITRUM, USDC_MAINNET } from '../../utils/test-data/assets' +import { ETH, ETH_ARBITRUM, FOX_MAINNET, USDC_ARBITRUM, USDC_MAINNET } from '../../utils/test-data/assets' import { getTradeQuote } from './getTradeQuote' vi.mock('@shapeshiftoss/contracts', () => ({ @@ -12,6 +12,9 @@ vi.mock('@shapeshiftoss/contracts', () => ({ [ethChainId]: { call: vi.fn().mockResolvedValue({ data: '0xdeadbeef' }), }, + [arbitrumChainId]: { + call: vi.fn().mockResolvedValue({ data: '0xdeadbeef' }), + }, }, })) @@ -164,4 +167,56 @@ describe('Stargate getTradeQuote', () => { const sendParamArg = vi.mocked(encodeSend).mock.calls[0][0] expect(sendParamArg.minAmountLD).toBe(985_050_000n) }) + + it('returns UnsupportedChain error for unsupported buy chain', async () => { + const result = await getTradeQuote( + { + ...commonInput, + buyAsset: { ...USDC_ARBITRUM, chainId: 'eip155:5' as const }, // Goerli (not supported) + }, + deps, + ) + expect(result.isErr()).toBe(true) + expect(result.unwrapErr().code).toBe(TradeQuoteError.UnsupportedChain) + }) + + it('returns UnsupportedTradePair error when sell asset has no Stargate contract', async () => { + // FOX has no Stargate pool on mainnet + const result = await getTradeQuote( + { ...commonInput, sellAsset: FOX_MAINNET }, + deps, + ) + expect(result.isErr()).toBe(true) + expect(result.unwrapErr().code).toBe(TradeQuoteError.UnsupportedTradePair) + }) + + it('returns a valid quote for ETH (mainnet) → ETH (Arbitrum) native bridge', async () => { + const result = await getTradeQuote( + { ...commonInput, sellAsset: ETH, buyAsset: ETH_ARBITRUM }, + deps, + ) + expect(result.isOk()).toBe(true) + + const step = result.unwrap()[0].steps[0] + expect(step.sellAsset.chainId).toBe(ethChainId) + expect(step.buyAsset.chainId).toBe(arbitrumChainId) + // native sell: txValue = nativeFee + sellAmount + // mocked nativeFee = 1_000_000_000_000_000, sellAmount = 1_000_000_000 + expect(step.stargateTransactionMetadata.value).toBe('1000001000000000') + }) + + it('returns a valid quote for USDC (Arbitrum) → USDC (Mainnet) reverse direction', async () => { + const result = await getTradeQuote( + { ...commonInput, sellAsset: USDC_ARBITRUM, buyAsset: USDC_MAINNET }, + deps, + ) + expect(result.isOk()).toBe(true) + + const step = result.unwrap()[0].steps[0] + expect(step.sellAsset.chainId).toBe(arbitrumChainId) + expect(step.buyAsset.chainId).toBe(ethChainId) + expect(step.source).toBe(SwapperName.Stargate) + // non-native sell: txValue = nativeFee only + expect(step.stargateTransactionMetadata.value).toBe('1000000000000000') + }) }) From 04350e63a5950df5d97c2bb3f7610b478da3dd9c Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 12 Apr 2026 15:20:38 +0200 Subject: [PATCH 12/17] test(stargate): expand asset coverage and add E2E fixture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit tests (20 total, +6 new pairs): - USDC ETH → Optimism, Base, Polygon - USDT ETH → USDT Arbitrum - ETH → ETH Base native bridge - USDC Base → USDC Arbitrum (L2-to-L2) test-data/assets.ts: - Add USDT_MAINNET, USDT_ARBITRUM - Add USDC_OPTIMISM, USDC_BASE, USDC_POLYGON - Add ETH_BASE, MATIC, BSC_USDC - Add baseChainId, polygonChainId imports E2E fixture: - stargate-usdc-eth-to-arb.yaml for agent-browser manual/automated QA Co-Authored-By: Claude Sonnet 4.6 --- e2e/fixtures/stargate-usdc-eth-to-arb.yaml | 102 +++++++++++++++ .../getTradeQuote/getTradeQuote.test.ts | 95 +++++++++++++- .../src/swappers/utils/test-data/assets.ts | 120 ++++++++++++++++++ 3 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 e2e/fixtures/stargate-usdc-eth-to-arb.yaml diff --git a/e2e/fixtures/stargate-usdc-eth-to-arb.yaml b/e2e/fixtures/stargate-usdc-eth-to-arb.yaml new file mode 100644 index 00000000000..c75bc5e8265 --- /dev/null +++ b/e2e/fixtures/stargate-usdc-eth-to-arb.yaml @@ -0,0 +1,102 @@ +name: Stargate USDC Ethereum → Arbitrum +description: > + Cross-chain USDC swap via Stargate V2 swapper: sell USDC on Ethereum mainnet, + receive USDC on Arbitrum One. Verifies that Stargate appears as a quote source, + that slippage is respected, and that the transaction builds and signs correctly. +route: /trade +depends_on: + - wallet-health.yaml + +steps: + - name: Dismiss stale notifications + instruction: > + Dismiss any lingering notifications, toasts, or feedback dialogs from + previous tests (close buttons, "Maybe Later", etc.). + expected: Clean trade page with no overlays + screenshot: true + + - name: Select USDC (Ethereum) as sell asset + instruction: > + Click the sell asset selector. Search for "USDC". From the results, + select "USD Coin (USDC)" on the Ethereum chain (not Arbitrum, not Base). + Verify USDC Ethereum is selected as the sell asset. + expected: USDC on Ethereum mainnet is the sell asset + screenshot: true + + - name: Select USDC (Arbitrum) as buy asset + instruction: > + Click the buy/receive asset selector. Search for "USDC". From the results, + expand USDC if needed and select "USD Coin (USDC)" on the Arbitrum One chain. + Verify USDC on Arbitrum is selected as the buy asset. + expected: USDC on Arbitrum One is the buy asset + screenshot: true + + - name: Toggle to fiat input mode + instruction: > + Click the "≈ $0.00" button below the sell amount to toggle to fiat/USD input mode. + If already in fiat mode (placeholder shows "$0"), skip this step. + expected: Sell input is in fiat/USD mode + screenshot: false + + - name: Enter swap amount ($1) + instruction: > + Click the sell amount input and type "1" character by character using press + (NOT fill). Wait for the value to register. + expected: $1.00 entered as sell amount + screenshot: true + + - name: Wait for Stargate quote + instruction: > + Wait up to 15 seconds for a quote to appear. Verify that: + 1. The "You Get" field shows a USDC amount close to $1 (slightly less due to fees) + 2. Stargate appears as the swapper source (look for "Stargate" label) + 3. The "Preview Trade" button is enabled + If multiple swappers are shown, check that Stargate is one of them. + expected: > + Quote shown with receive amount ~$0.98-$1.00 USDC, Stargate visible as source, + Preview Trade button enabled + screenshot: true + + - name: Verify protocol fee is displayed + instruction: > + In the quote view, look for a fee breakdown section. Verify that a protocol fee + is shown (the Stargate OFT fee, typically 0.5-1% of the swap amount). + Note the fee amount shown. + expected: Protocol fee visible, roughly $0.005-$0.01 for a $1 swap + screenshot: true + + - name: Preview trade + instruction: > + Click the "Preview Trade" button. Wait for the "Confirm Details" screen to appear + showing: sell amount (USDC Ethereum), receive amount (USDC Arbitrum), swapper name + (Stargate), estimated fees, and a "Confirm and Trade" button. + expected: > + Confirm Details screen visible with cross-chain summary: + sell = USDC on Ethereum, receive = USDC on Arbitrum, swapper = Stargate + screenshot: true + + - name: Confirm and sign + instruction: > + Click the "Confirm and Trade" button. Wait up to 30 seconds for the + "Sign & Swap" button to become enabled (not loading/disabled). + Then click "Sign & Swap". The native wallet signs automatically. + expected: Transaction submitted, cross-chain swap in progress + screenshot: true + + - name: Wait for Stargate bridge completion + instruction: > + Cross-chain Stargate bridges take 1-3 minutes (LayerZero messaging). + Wait up to 180 seconds for the swap to complete. Check every 10 seconds. + Look for: trade page reappearing, success notification, Arbitrum USDC + balance increased, or "Awaiting swap" disappearing. + A feedback dialog may appear — dismiss it with "Maybe Later". + expected: > + Swap completed — back on trade page, USDC balance on Arbitrum increased, + no "Awaiting swap" visible + screenshot: true + + - name: Clean up notifications + instruction: > + Dismiss any remaining notifications, feedback dialogs, or toasts. + expected: Clean trade page, ready for next test + screenshot: true diff --git a/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts index fc33632d7db..7833bd2c8b3 100644 --- a/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts +++ b/packages/swapper/src/swappers/StargateSwapper/getTradeQuote/getTradeQuote.test.ts @@ -1,10 +1,22 @@ -import { arbitrumChainId, ethChainId } from '@shapeshiftoss/caip' +import { arbitrumChainId, baseChainId, ethChainId, optimismChainId, polygonChainId } from '@shapeshiftoss/caip' import type { EvmChainAdapter } from '@shapeshiftoss/chain-adapters' import { describe, expect, it, vi } from 'vitest' import type { GetEvmTradeQuoteInputBase, SwapperDeps } from '../../../types' import { SwapperName, TradeQuoteError } from '../../../types' -import { ETH, ETH_ARBITRUM, FOX_MAINNET, USDC_ARBITRUM, USDC_MAINNET } from '../../utils/test-data/assets' +import { + ETH, + ETH_ARBITRUM, + ETH_BASE, + FOX_MAINNET, + USDC_ARBITRUM, + USDC_BASE, + USDC_MAINNET, + USDC_OPTIMISM, + USDC_POLYGON, + USDT_ARBITRUM, + USDT_MAINNET, +} from '../../utils/test-data/assets' import { getTradeQuote } from './getTradeQuote' vi.mock('@shapeshiftoss/contracts', () => ({ @@ -15,6 +27,15 @@ vi.mock('@shapeshiftoss/contracts', () => ({ [arbitrumChainId]: { call: vi.fn().mockResolvedValue({ data: '0xdeadbeef' }), }, + [optimismChainId]: { + call: vi.fn().mockResolvedValue({ data: '0xdeadbeef' }), + }, + [baseChainId]: { + call: vi.fn().mockResolvedValue({ data: '0xdeadbeef' }), + }, + [polygonChainId]: { + call: vi.fn().mockResolvedValue({ data: '0xdeadbeef' }), + }, }, })) @@ -219,4 +240,74 @@ describe('Stargate getTradeQuote', () => { // non-native sell: txValue = nativeFee only expect(step.stargateTransactionMetadata.value).toBe('1000000000000000') }) + + it('returns a valid quote for USDC (Mainnet) → USDC (Optimism)', async () => { + const result = await getTradeQuote( + { ...commonInput, sellAsset: USDC_MAINNET, buyAsset: USDC_OPTIMISM }, + deps, + ) + expect(result.isOk()).toBe(true) + const step = result.unwrap()[0].steps[0] + expect(step.sellAsset.chainId).toBe(ethChainId) + expect(step.buyAsset.chainId).toBe(optimismChainId) + }) + + it('returns a valid quote for USDC (Mainnet) → USDC (Base)', async () => { + const result = await getTradeQuote( + { ...commonInput, sellAsset: USDC_MAINNET, buyAsset: USDC_BASE }, + deps, + ) + expect(result.isOk()).toBe(true) + const step = result.unwrap()[0].steps[0] + expect(step.sellAsset.chainId).toBe(ethChainId) + expect(step.buyAsset.chainId).toBe(baseChainId) + }) + + it('returns a valid quote for USDC (Mainnet) → USDC (Polygon)', async () => { + const result = await getTradeQuote( + { ...commonInput, sellAsset: USDC_MAINNET, buyAsset: USDC_POLYGON }, + deps, + ) + expect(result.isOk()).toBe(true) + const step = result.unwrap()[0].steps[0] + expect(step.sellAsset.chainId).toBe(ethChainId) + expect(step.buyAsset.chainId).toBe(polygonChainId) + }) + + it('returns a valid quote for USDT (Mainnet) → USDT (Arbitrum)', async () => { + const result = await getTradeQuote( + { ...commonInput, sellAsset: USDT_MAINNET, buyAsset: USDT_ARBITRUM }, + deps, + ) + expect(result.isOk()).toBe(true) + const step = result.unwrap()[0].steps[0] + expect(step.sellAsset.chainId).toBe(ethChainId) + expect(step.buyAsset.chainId).toBe(arbitrumChainId) + expect(step.sellAsset.symbol).toBe('USDT') + }) + + it('returns a valid quote for ETH (Mainnet) → ETH (Base) native bridge', async () => { + const result = await getTradeQuote( + { ...commonInput, sellAsset: ETH, buyAsset: ETH_BASE }, + deps, + ) + expect(result.isOk()).toBe(true) + const step = result.unwrap()[0].steps[0] + expect(step.sellAsset.chainId).toBe(ethChainId) + expect(step.buyAsset.chainId).toBe(baseChainId) + // native sell: txValue = nativeFee + sellAmount + expect(step.stargateTransactionMetadata.value).toBe('1000001000000000') + }) + + it('returns a valid quote for USDC (Base) → USDC (Arbitrum) L2-to-L2', async () => { + const result = await getTradeQuote( + { ...commonInput, sellAsset: USDC_BASE, buyAsset: USDC_ARBITRUM }, + deps, + ) + expect(result.isOk()).toBe(true) + const step = result.unwrap()[0].steps[0] + expect(step.sellAsset.chainId).toBe(baseChainId) + expect(step.buyAsset.chainId).toBe(arbitrumChainId) + expect(step.source).toBe(SwapperName.Stargate) + }) }) diff --git a/packages/swapper/src/swappers/utils/test-data/assets.ts b/packages/swapper/src/swappers/utils/test-data/assets.ts index 011a6048e05..8008c74c270 100644 --- a/packages/swapper/src/swappers/utils/test-data/assets.ts +++ b/packages/swapper/src/swappers/utils/test-data/assets.ts @@ -2,6 +2,8 @@ import { arbitrumChainId, avalancheAssetId, avalancheChainId, + baseAssetId, + baseChainId, bscAssetId, bscChainId, ethAssetId, @@ -13,6 +15,8 @@ import { gnosisChainId, optimismAssetId, optimismChainId, + polygonAssetId, + polygonChainId, thorchainAssetId, thorchainChainId, } from '@shapeshiftoss/caip' @@ -252,3 +256,119 @@ export const RUNE: Asset = { explorerTxLink: 'https://viewblock.io/thorchain/tx/', relatedAssetKey: null, } + +// Stargate-supported assets + +export const USDT_MAINNET: Asset = { + assetId: 'eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7', + chainId: ethChainId, + symbol: 'USDT', + name: 'Tether USD', + precision: 6, + color: '#26A17B', + icon: 'https://assets.coingecko.com/coins/images/325/thumb/Tether.png?1668148663', + explorer: 'https://etherscan.io', + explorerAddressLink: 'https://etherscan.io/address/', + explorerTxLink: 'https://etherscan.io/tx/', + relatedAssetKey: null, +} + +export const USDT_ARBITRUM: Asset = { + assetId: 'eip155:42161/erc20:0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9', + chainId: arbitrumChainId, + symbol: 'USDT', + name: 'Tether USD on Arbitrum', + precision: 6, + color: '#26A17B', + icon: 'https://assets.coingecko.com/coins/images/325/thumb/Tether.png?1668148663', + explorer: 'https://arbiscan.io', + explorerAddressLink: 'https://arbiscan.io/address/', + explorerTxLink: 'https://arbiscan.io/tx/', + relatedAssetKey: null, +} + +export const USDC_OPTIMISM: Asset = { + assetId: 'eip155:10/erc20:0x0b2c639c533813f4aa9d7837caf62653d097ff85', + chainId: optimismChainId, + symbol: 'USDC', + name: 'USD Coin on Optimism', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + explorer: 'https://optimistic.etherscan.io', + explorerAddressLink: 'https://optimistic.etherscan.io/address/', + explorerTxLink: 'https://optimistic.etherscan.io/tx/', + relatedAssetKey: null, +} + +export const USDC_BASE: Asset = { + assetId: 'eip155:8453/erc20:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', + chainId: baseChainId, + symbol: 'USDC', + name: 'USD Coin on Base', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + explorer: 'https://basescan.org', + explorerAddressLink: 'https://basescan.org/address/', + explorerTxLink: 'https://basescan.org/tx/', + relatedAssetKey: null, +} + +export const USDC_POLYGON: Asset = { + assetId: 'eip155:137/erc20:0x3c499c542cef5e3811e1192ce70d8cc03d5c3359', + chainId: polygonChainId, + symbol: 'USDC', + name: 'USD Coin on Polygon', + precision: 6, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + explorer: 'https://polygonscan.com', + explorerAddressLink: 'https://polygonscan.com/address/', + explorerTxLink: 'https://polygonscan.com/tx/', + relatedAssetKey: null, +} + +export const ETH_BASE: Asset = { + assetId: baseAssetId, + chainId: baseChainId, + name: 'Ethereum on Base', + networkName: 'Base', + symbol: 'ETH', + precision: 18, + color: '#5C6BC0', + networkColor: '#0052FF', + icon: 'https://rawcdn.githack.com/trustwallet/assets/32e51d582a890b3dd3135fe3ee7c20c2fd699a6d/blockchains/ethereum/info/logo.png', + explorer: 'https://basescan.org', + explorerAddressLink: 'https://basescan.org/address/', + explorerTxLink: 'https://basescan.org/tx/', + relatedAssetKey: null, +} + +export const MATIC: Asset = { + assetId: polygonAssetId, + chainId: polygonChainId, + name: 'Polygon', + symbol: 'MATIC', + precision: 18, + color: '#8247E5', + icon: 'https://assets.coingecko.com/coins/images/4713/thumb/polygon.png?1698233745', + explorer: 'https://polygonscan.com', + explorerAddressLink: 'https://polygonscan.com/address/', + explorerTxLink: 'https://polygonscan.com/tx/', + relatedAssetKey: null, +} + +export const BSC_USDC: Asset = { + assetId: 'eip155:56/erc20:0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d', + chainId: bscChainId, + symbol: 'USDC', + name: 'USD Coin on BNB Smart Chain', + precision: 18, + color: '#2373CB', + icon: 'https://rawcdn.githack.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png', + explorer: 'https://bscscan.com', + explorerAddressLink: 'https://bscscan.com/address/', + explorerTxLink: 'https://bscscan.com/tx/', + relatedAssetKey: null, +} From a9c8e5d566b66b2bda140327fdce34e999184e82 Mon Sep 17 00:00:00 2001 From: Discostu Date: Sun, 12 Apr 2026 16:35:33 +0200 Subject: [PATCH 13/17] feat: enable Stargate V2 swapper and fix E2E wallet-health fixture Enable VITE_FEATURE_STARGATE_SWAP feature flag. Stargate V2 is fully implemented with cross-chain USDC bridging via LayerZero, ERC-20 approval flow, and slippage handling. Also fix wallet-health.yaml to use `fill` for password input instead of JS eval + press char-by-char, which was unreliable and based on an outdated assumption about React controlled inputs. Co-Authored-By: Claude Sonnet 4.6 --- .env | 2 +- e2e/fixtures/wallet-health.yaml | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 260212f7038..5d906269765 100644 --- a/.env +++ b/.env @@ -6,7 +6,7 @@ VITE_FEATURE_POLYGON=true VITE_FEATURE_GNOSIS=true VITE_FEATURE_ARBITRUM=true VITE_FEATURE_SOLANA=true -VITE_FEATURE_STARGATE_SWAP=false +VITE_FEATURE_STARGATE_SWAP=true VITE_FEATURE_STARKNET=true VITE_FEATURE_SUI=true VITE_FEATURE_MAYACHAIN=true diff --git a/e2e/fixtures/wallet-health.yaml b/e2e/fixtures/wallet-health.yaml index 31b3d8eb41c..c08b4b6410f 100644 --- a/e2e/fixtures/wallet-health.yaml +++ b/e2e/fixtures/wallet-health.yaml @@ -16,12 +16,9 @@ steps: - name: Fill wallet password instruction: > Wait for the "Enter Your Password" dialog to appear. - Click on the native wallet button (e.g. "teest" or "test") to select it if not already selected. - Once the password input and "Next" button appear, focus the password input using JS eval - (click --ref often times out on external origins): - eval "document.querySelector('input[type=password], input[placeholder*=Password]')?.focus()" - Then type $NATIVE_WALLET_PASSWORD character by character using press - (agent-browser fill does not trigger React onChange - use press for each char). + Click on the native wallet button (e.g. "test-wallet") to select it if not already selected. + Once the password input and "Next" button appear, use fill to enter the password: + fill "input[placeholder='Enter Password']" "$NATIVE_WALLET_PASSWORD" expected: Password field is filled (shows masked dots), Next button is enabled and clickable screenshot: true - name: Unlock wallet From 5efb8020c709ced872ec7220be4ae48d7c8bdc83 Mon Sep 17 00:00:00 2001 From: Discostu Date: Tue, 5 May 2026 21:03:17 +0200 Subject: [PATCH 14/17] fix(stargate): address CodeRabbit review comments --- .env | 2 +- e2e/fixtures/stargate-usdc-eth-to-arb.yaml | 8 +-- .../src/swappers/StargateSwapper/constant.ts | 2 + .../getTradeQuote/getTradeQuote.test.ts | 50 +++++++++---------- .../utils/fetchStargateTrade.ts | 10 ++-- 5 files changed, 37 insertions(+), 35 deletions(-) diff --git a/.env b/.env index 5d906269765..8924a93d47d 100644 --- a/.env +++ b/.env @@ -365,4 +365,4 @@ VITE_DEBRIDGE_API_URL=https://dln.debridge.finance/v1.0 VITE_USERBACK_TOKEN=A-3gHopRTd55QqxXGsJd0XLVVG3 # agentic chat -VITE_AGENTIC_SERVER_BASE_URL=https://api.agent.shapeshift.com \ No newline at end of file +VITE_AGENTIC_SERVER_BASE_URL=https://api.agent.shapeshift.com diff --git a/e2e/fixtures/stargate-usdc-eth-to-arb.yaml b/e2e/fixtures/stargate-usdc-eth-to-arb.yaml index c75bc5e8265..fdd09368584 100644 --- a/e2e/fixtures/stargate-usdc-eth-to-arb.yaml +++ b/e2e/fixtures/stargate-usdc-eth-to-arb.yaml @@ -53,8 +53,8 @@ steps: 3. The "Preview Trade" button is enabled If multiple swappers are shown, check that Stargate is one of them. expected: > - Quote shown with receive amount ~$0.98-$1.00 USDC, Stargate visible as source, - Preview Trade button enabled + Quote shown with a USDC receive amount slightly less than sell amount, + Stargate visible as source, Preview Trade button enabled screenshot: true - name: Verify protocol fee is displayed @@ -62,7 +62,7 @@ steps: In the quote view, look for a fee breakdown section. Verify that a protocol fee is shown (the Stargate OFT fee, typically 0.5-1% of the swap amount). Note the fee amount shown. - expected: Protocol fee visible, roughly $0.005-$0.01 for a $1 swap + expected: Protocol fee visible in the quote breakdown screenshot: true - name: Preview trade @@ -86,7 +86,7 @@ steps: - name: Wait for Stargate bridge completion instruction: > Cross-chain Stargate bridges take 1-3 minutes (LayerZero messaging). - Wait up to 180 seconds for the swap to complete. Check every 10 seconds. + Wait up to 300 seconds for the swap to complete. Check every 10 seconds. Look for: trade page reappearing, success notification, Arbitrum USDC balance increased, or "Awaiting swap" disappearing. A feedback dialog may appear — dismiss it with "Maybe Later". diff --git a/packages/swapper/src/swappers/StargateSwapper/constant.ts b/packages/swapper/src/swappers/StargateSwapper/constant.ts index 6c8bf497b32..479202841ca 100644 --- a/packages/swapper/src/swappers/StargateSwapper/constant.ts +++ b/packages/swapper/src/swappers/StargateSwapper/constant.ts @@ -103,3 +103,5 @@ export const stargateContractsByChainAndAsset: Record { vi.mock('../utils/helpers', () => ({ encodeQuoteOFT: vi.fn().mockReturnValue('0x11'), - decodeQuoteOFTResult: vi.fn().mockReturnValue([ - {}, - [], - { amountReceivedLD: 990_000_000n, amountSentLD: 1_000_000_000n }, - ]), + decodeQuoteOFTResult: vi + .fn() + .mockReturnValue([{}, [], { amountReceivedLD: 990_000_000n, amountSentLD: 1_000_000_000n }]), encodeQuoteSend: vi.fn().mockReturnValue('0x22'), decodeQuoteSendResult: vi.fn().mockReturnValue({ nativeFee: 1_000_000_000_000_000n, @@ -96,7 +100,10 @@ describe('Stargate getTradeQuote', () => { }) it('returns error when receiveAddress is missing', async () => { - const result = await getTradeQuote({ ...commonInput, receiveAddress: undefined }, deps) + const result = await getTradeQuote( + { ...commonInput, receiveAddress: undefined as unknown as string }, + deps, + ) expect(result.isErr()).toBe(true) const err = result.unwrapErr() expect(err.message).toBe('receiveAddress is required') @@ -104,10 +111,7 @@ describe('Stargate getTradeQuote', () => { }) it('returns UnsupportedTradePair error for same-chain swap', async () => { - const result = await getTradeQuote( - { ...commonInput, buyAsset: USDC_MAINNET }, - deps, - ) + const result = await getTradeQuote({ ...commonInput, buyAsset: USDC_MAINNET }, deps) expect(result.isErr()).toBe(true) expect(result.unwrapErr().code).toBe(TradeQuoteError.UnsupportedTradePair) }) @@ -155,10 +159,10 @@ describe('Stargate getTradeQuote', () => { expect(step.feeData.networkFeeCryptoBaseUnit).toBeDefined() expect(step.feeData.protocolFees).toBeDefined() - const protocolFee = step.feeData.protocolFees[USDC_MAINNET.assetId] + const protocolFee = step.feeData.protocolFees?.[USDC_MAINNET.assetId] expect(protocolFee).toBeDefined() // fee = amountSentLD - amountReceivedLD = 1_000_000_000 - 990_000_000 = 10_000_000 - expect(protocolFee.amountCryptoBaseUnit).toBe('10000000') + expect(protocolFee?.amountCryptoBaseUnit).toBe('10000000') }) it('quote step has stargateTransactionMetadata', async () => { @@ -166,8 +170,8 @@ describe('Stargate getTradeQuote', () => { const step = result.unwrap()[0].steps[0] expect(step.stargateTransactionMetadata).toBeDefined() - expect(step.stargateTransactionMetadata.to).toBeDefined() - expect(step.stargateTransactionMetadata.data).toBe('0x33') + expect(step.stargateTransactionMetadata?.to).toBeDefined() + expect(step.stargateTransactionMetadata?.data).toBe('0x33') }) it('buyAmountAfterFees reflects the received amount from quoteOFT', async () => { @@ -203,10 +207,7 @@ describe('Stargate getTradeQuote', () => { it('returns UnsupportedTradePair error when sell asset has no Stargate contract', async () => { // FOX has no Stargate pool on mainnet - const result = await getTradeQuote( - { ...commonInput, sellAsset: FOX_MAINNET }, - deps, - ) + const result = await getTradeQuote({ ...commonInput, sellAsset: FOX_MAINNET }, deps) expect(result.isErr()).toBe(true) expect(result.unwrapErr().code).toBe(TradeQuoteError.UnsupportedTradePair) }) @@ -223,7 +224,7 @@ describe('Stargate getTradeQuote', () => { expect(step.buyAsset.chainId).toBe(arbitrumChainId) // native sell: txValue = nativeFee + sellAmount // mocked nativeFee = 1_000_000_000_000_000, sellAmount = 1_000_000_000 - expect(step.stargateTransactionMetadata.value).toBe('1000001000000000') + expect(step.stargateTransactionMetadata?.value).toBe('1000001000000000') }) it('returns a valid quote for USDC (Arbitrum) → USDC (Mainnet) reverse direction', async () => { @@ -238,7 +239,7 @@ describe('Stargate getTradeQuote', () => { expect(step.buyAsset.chainId).toBe(ethChainId) expect(step.source).toBe(SwapperName.Stargate) // non-native sell: txValue = nativeFee only - expect(step.stargateTransactionMetadata.value).toBe('1000000000000000') + expect(step.stargateTransactionMetadata?.value).toBe('1000000000000000') }) it('returns a valid quote for USDC (Mainnet) → USDC (Optimism)', async () => { @@ -287,16 +288,13 @@ describe('Stargate getTradeQuote', () => { }) it('returns a valid quote for ETH (Mainnet) → ETH (Base) native bridge', async () => { - const result = await getTradeQuote( - { ...commonInput, sellAsset: ETH, buyAsset: ETH_BASE }, - deps, - ) + const result = await getTradeQuote({ ...commonInput, sellAsset: ETH, buyAsset: ETH_BASE }, deps) expect(result.isOk()).toBe(true) const step = result.unwrap()[0].steps[0] expect(step.sellAsset.chainId).toBe(ethChainId) expect(step.buyAsset.chainId).toBe(baseChainId) // native sell: txValue = nativeFee + sellAmount - expect(step.stargateTransactionMetadata.value).toBe('1000001000000000') + expect(step.stargateTransactionMetadata?.value).toBe('1000001000000000') }) it('returns a valid quote for USDC (Base) → USDC (Arbitrum) L2-to-L2', async () => { diff --git a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts index c243490b38d..4bb0e093783 100644 --- a/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts +++ b/packages/swapper/src/swappers/StargateSwapper/utils/fetchStargateTrade.ts @@ -21,6 +21,7 @@ import { getInputOutputRate, makeSwapErrorRight } from '../../../utils' import { isNativeEvmAsset } from '../../utils/helpers/helpers' import { chainIdToStargateEndpointId, + DEFAULT_STARGATE_GAS_LIMIT, DEFAULT_STARGATE_USER_ADDRESS, STARGATE_NATIVE_ASSET_ADDRESS, STARGATE_SUPPORTED_CHAIN_IDS, @@ -238,9 +239,10 @@ export async function fetchStargateTrade({ ) } - const { nativeFee, lzTokenFee } = decodeQuoteSendResult( - quoteSendResult.data as Hex, - ) as { nativeFee: bigint; lzTokenFee: bigint } + const { nativeFee, lzTokenFee } = decodeQuoteSendResult(quoteSendResult.data as Hex) as { + nativeFee: bigint + lzTokenFee: bigint + } const messagingFee: StargateMessagingFee = { nativeFee, lzTokenFee } const buyAmountAfterFeesCryptoBaseUnit = detailDstAmountLD.toString() @@ -272,7 +274,7 @@ export async function fetchStargateTrade({ const { average } = await adapter.getGasFeeData() const supportsEIP1559 = 'maxFeePerGas' in average - let gasLimit = '500000' + let gasLimit = DEFAULT_STARGATE_GAS_LIMIT const networkFeeCryptoBaseUnit = await (async () => { try { const feeData = await evm.getFees({ From 8367001c2b156b3472e0526301dc12218210bd72 Mon Sep 17 00:00:00 2001 From: Discostu Date: Wed, 6 May 2026 01:04:31 +0200 Subject: [PATCH 15/17] chore(stargate): remove unrelated agent fixture changes --- AGENTS.md | 6 ------ e2e/fixtures/wallet-health.yaml | 9 ++++++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ea942890364..2141357a451 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,12 +21,6 @@ This file is the canonical instruction entrypoint for local agent tooling in thi - Prefer `origin` for new feature/fix branch pushes when permissions allow. - If a PR branch is already on `fork`, keep using that existing `fork` branch. - Run `pnpm run lint --fix` and `pnpm run type-check` after code changes. -- **Before running tests**: generate `unchained-client` OpenAPI clients first (requires Java). Tests fail with `./generated/ethereum/runtime not found` without this step: - ```bash - JAVA_HOME=/tmp/jdk-21.0.10+7-jre/Contents/Home PATH="$JAVA_HOME/bin:$PATH" \ - pnpm --filter @shapeshiftoss/unchained-client generate - ``` -- **Test mock pattern**: always use `vi.importActual` for real packages, only override specific functions. - Use `.github/PULL_REQUEST_TEMPLATE.md` for PR bodies. ## Routing diff --git a/e2e/fixtures/wallet-health.yaml b/e2e/fixtures/wallet-health.yaml index c08b4b6410f..31b3d8eb41c 100644 --- a/e2e/fixtures/wallet-health.yaml +++ b/e2e/fixtures/wallet-health.yaml @@ -16,9 +16,12 @@ steps: - name: Fill wallet password instruction: > Wait for the "Enter Your Password" dialog to appear. - Click on the native wallet button (e.g. "test-wallet") to select it if not already selected. - Once the password input and "Next" button appear, use fill to enter the password: - fill "input[placeholder='Enter Password']" "$NATIVE_WALLET_PASSWORD" + Click on the native wallet button (e.g. "teest" or "test") to select it if not already selected. + Once the password input and "Next" button appear, focus the password input using JS eval + (click --ref often times out on external origins): + eval "document.querySelector('input[type=password], input[placeholder*=Password]')?.focus()" + Then type $NATIVE_WALLET_PASSWORD character by character using press + (agent-browser fill does not trigger React onChange - use press for each char). expected: Password field is filled (shows masked dots), Next button is enabled and clickable screenshot: true - name: Unlock wallet From 15f9c708930ce764cc0acf670783a7fe56cf2779 Mon Sep 17 00:00:00 2001 From: Discostu Date: Wed, 6 May 2026 10:15:25 +0200 Subject: [PATCH 16/17] test(e2e): force english locale in qa fixtures --- e2e/fixtures/abstract-chain.yaml | 2 +- e2e/fixtures/asset-data-regression.yaml | 2 +- e2e/fixtures/bebop-solana-swap.yaml | 2 +- e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml | 2 +- e2e/fixtures/chainflip-lending-revamp-ui.yaml | 2 +- e2e/fixtures/chainflip-lending.yaml | 2 +- e2e/fixtures/eth-to-fox-swap.yaml | 2 +- e2e/fixtures/evm-chains-regression.yaml | 2 +- e2e/fixtures/fox-ecosystem.yaml | 2 +- e2e/fixtures/send-receive.yaml | 2 +- e2e/fixtures/stargate-usdc-eth-to-arb.yaml | 2 +- e2e/fixtures/swap-exploration.yaml | 2 +- e2e/fixtures/thorchain-solana-swapper.yaml | 2 +- e2e/fixtures/trade-exploration.yaml | 2 +- e2e/fixtures/tron-non-activated-balances.yaml | 2 +- e2e/fixtures/wallet-health.yaml | 2 +- e2e/fixtures/yield-e2e.yaml | 2 +- e2e/fixtures/yield-lending.yaml | 2 +- e2e/fixtures/yield-staking.yaml | 2 +- e2e/fixtures/yield-vault.yaml | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/e2e/fixtures/abstract-chain.yaml b/e2e/fixtures/abstract-chain.yaml index 02a0b3cc599..5c97f09d76a 100644 --- a/e2e/fixtures/abstract-chain.yaml +++ b/e2e/fixtures/abstract-chain.yaml @@ -1,6 +1,6 @@ name: Abstract Chain Integration description: Verify Abstract chain assets appear in swapper, search works, and ETH->Abstract swap quote can be obtained -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/asset-data-regression.yaml b/e2e/fixtures/asset-data-regression.yaml index 3e6b15dc3a3..d51628e3210 100644 --- a/e2e/fixtures/asset-data-regression.yaml +++ b/e2e/fixtures/asset-data-regression.yaml @@ -4,7 +4,7 @@ description: > Tests USDC, USDT, and FOX search with grouped multi-chain variants via global search, plus markets recently added section. Does NOT require wallet connection - no depends_on smoke-test. -route: /trade +route: /trade?lang=en # ============================================================ # IMPLEMENTATION NOTES (for agent-browser execution): diff --git a/e2e/fixtures/bebop-solana-swap.yaml b/e2e/fixtures/bebop-solana-swap.yaml index 7f963a1ad88..3b9085a75f6 100644 --- a/e2e/fixtures/bebop-solana-swap.yaml +++ b/e2e/fixtures/bebop-solana-swap.yaml @@ -5,7 +5,7 @@ description: > THORChain LP tests the compute budget fix for memo transactions. Upstream Bebop errors (InsufficientLiquidity code 102, TransactionExecutionError code 208) are expected soft_fails, not our bugs. -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml b/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml index 8e1c702969e..dc4935d5082 100644 --- a/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml +++ b/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml @@ -4,7 +4,7 @@ description: > already executed Chainflip lending USDC operations and the Action Center contains the resulting cards. Focuses only on notification center copy, expanded details, and egress transaction affordances. -route: /#/chainflip-lending/pool/eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 +route: /#/chainflip-lending/pool/eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48?lang=en steps: - name: Verify confirmed Chainflip lending cards instruction: > diff --git a/e2e/fixtures/chainflip-lending-revamp-ui.yaml b/e2e/fixtures/chainflip-lending-revamp-ui.yaml index 5b6ad6534af..77a1b532fec 100644 --- a/e2e/fixtures/chainflip-lending-revamp-ui.yaml +++ b/e2e/fixtures/chainflip-lending-revamp-ui.yaml @@ -1,6 +1,6 @@ name: Chainflip Lending Revamp UI description: Validates the revamped Chainflip lending dashboard, init view, funded view with sections, and key action modals. -route: /chainflip-lending +route: /chainflip-lending?lang=en steps: # === No-wallet state === - name: Landing page without wallet diff --git a/e2e/fixtures/chainflip-lending.yaml b/e2e/fixtures/chainflip-lending.yaml index 7e1740c38e9..453523f6ea8 100644 --- a/e2e/fixtures/chainflip-lending.yaml +++ b/e2e/fixtures/chainflip-lending.yaml @@ -78,7 +78,7 @@ description: > At each signing step, a "Confirm" button may appear for hardware wallets. Click it. If password prompt: enter $NATIVE_WALLET_PASSWORD. -route: /chainflip-lending +route: /chainflip-lending?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/eth-to-fox-swap.yaml b/e2e/fixtures/eth-to-fox-swap.yaml index 92a37a04ce3..0427789ba67 100644 --- a/e2e/fixtures/eth-to-fox-swap.yaml +++ b/e2e/fixtures/eth-to-fox-swap.yaml @@ -1,6 +1,6 @@ name: ETH to FOX Swap description: Full ETH to FOX swap - select assets, enter amount, preview, sign, wait for completion -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/evm-chains-regression.yaml b/e2e/fixtures/evm-chains-regression.yaml index 2f707a8040f..68ee35fa341 100644 --- a/e2e/fixtures/evm-chains-regression.yaml +++ b/e2e/fixtures/evm-chains-regression.yaml @@ -4,7 +4,7 @@ description: > on each chain (native -> any token) and verifies the swap completes. Detects node failures, RPC issues, and swapper regressions. Chains: Ethereum, Base, Gnosis, Avalanche, Arbitrum, BSC, Optimism. -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/fox-ecosystem.yaml b/e2e/fixtures/fox-ecosystem.yaml index 2b63b05ce83..cf6fd6bc521 100644 --- a/e2e/fixtures/fox-ecosystem.yaml +++ b/e2e/fixtures/fox-ecosystem.yaml @@ -1,6 +1,6 @@ name: fox-ecosystem description: Fox Ecosystem page - rFOX staking/unstaking/claiming, simulator, FOX token, farming, governance, action center notifications -route: /fox-ecosystem +route: /fox-ecosystem?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/send-receive.yaml b/e2e/fixtures/send-receive.yaml index b8776f57d9a..67d202f07e5 100644 --- a/e2e/fixtures/send-receive.yaml +++ b/e2e/fixtures/send-receive.yaml @@ -79,7 +79,7 @@ description: > - Total steps = wallet-health deps + (chains * steps_per_chain) - If a chain fails, log the failure and CONTINUE to the next chain. -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/stargate-usdc-eth-to-arb.yaml b/e2e/fixtures/stargate-usdc-eth-to-arb.yaml index fdd09368584..36e5d73fec7 100644 --- a/e2e/fixtures/stargate-usdc-eth-to-arb.yaml +++ b/e2e/fixtures/stargate-usdc-eth-to-arb.yaml @@ -3,7 +3,7 @@ description: > Cross-chain USDC swap via Stargate V2 swapper: sell USDC on Ethereum mainnet, receive USDC on Arbitrum One. Verifies that Stargate appears as a quote source, that slippage is respected, and that the transaction builds and signs correctly. -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/swap-exploration.yaml b/e2e/fixtures/swap-exploration.yaml index f5939b91943..8a03b370e29 100644 --- a/e2e/fixtures/swap-exploration.yaml +++ b/e2e/fixtures/swap-exploration.yaml @@ -40,7 +40,7 @@ description: > - Track which pairs worked and which failed mode: exploratory -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/thorchain-solana-swapper.yaml b/e2e/fixtures/thorchain-solana-swapper.yaml index 27b6b5a9b13..86eff781d9b 100644 --- a/e2e/fixtures/thorchain-solana-swapper.yaml +++ b/e2e/fixtures/thorchain-solana-swapper.yaml @@ -1,6 +1,6 @@ name: THORChain Solana Swapper description: Test THORChain Solana swapper integration - SOL to RUNE and RUNE to SOL swaps, specifically selecting THORChain quotes -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/trade-exploration.yaml b/e2e/fixtures/trade-exploration.yaml index 2e57c0f3976..afaf125c847 100644 --- a/e2e/fixtures/trade-exploration.yaml +++ b/e2e/fixtures/trade-exploration.yaml @@ -9,7 +9,7 @@ description: > "Found: infinite loading when switching assets rapidly" mode: exploratory -route: /trade +route: /trade?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/tron-non-activated-balances.yaml b/e2e/fixtures/tron-non-activated-balances.yaml index 63aeda9ae76..f985191e439 100644 --- a/e2e/fixtures/tron-non-activated-balances.yaml +++ b/e2e/fixtures/tron-non-activated-balances.yaml @@ -2,7 +2,7 @@ name: Tron Non-Activated Account Balances description: > Verifies fix for #12190 - USDT/TRC20 tokens show in portfolio for non-activated Tron accounts, and send flow shows a warning when sending TRC20 to a non-activated recipient address. -route: / +route: /?lang=en depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/wallet-health.yaml b/e2e/fixtures/wallet-health.yaml index 31b3d8eb41c..db3e1624d42 100644 --- a/e2e/fixtures/wallet-health.yaml +++ b/e2e/fixtures/wallet-health.yaml @@ -1,6 +1,6 @@ name: Wallet Health description: Dismiss onboarding, unlock native wallet, verify trade page loads with swap inputs and button -route: /trade +route: /trade?lang=en steps: - name: Dismiss onboarding and banners instruction: > diff --git a/e2e/fixtures/yield-e2e.yaml b/e2e/fixtures/yield-e2e.yaml index c832567f5b4..f525ede4132 100644 --- a/e2e/fixtures/yield-e2e.yaml +++ b/e2e/fixtures/yield-e2e.yaml @@ -165,7 +165,7 @@ description: > Compare APY, TVL, provider name, asset name against what the UI shows. Any mismatch is a SOFT FAIL bug worth reporting. -route: /earn +route: /earn?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/yield-lending.yaml b/e2e/fixtures/yield-lending.yaml index 646935f7091..174d34f0d1c 100644 --- a/e2e/fixtures/yield-lending.yaml +++ b/e2e/fixtures/yield-lending.yaml @@ -66,7 +66,7 @@ description: > 4. Preview, confirm, sign, wait 120s. Navigate back. 5. Report: "FUNDING SWAP: $0.20 {source} → {target}" -route: /earn +route: /earn?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/yield-staking.yaml b/e2e/fixtures/yield-staking.yaml index d3848fb852d..28a6019a154 100644 --- a/e2e/fixtures/yield-staking.yaml +++ b/e2e/fixtures/yield-staking.yaml @@ -63,7 +63,7 @@ description: > - window.location.hash for navigation - Native wallet signs automatically -route: /earn +route: /earn?lang=en depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/yield-vault.yaml b/e2e/fixtures/yield-vault.yaml index 2d26a7faa26..dbe23d1ed17 100644 --- a/e2e/fixtures/yield-vault.yaml +++ b/e2e/fixtures/yield-vault.yaml @@ -52,7 +52,7 @@ description: > 3. FIAT $0.20 (enough for a yield deposit). Max $3. 4. Preview, confirm, sign, wait 120s. Navigate back. -route: /earn +route: /earn?lang=en depends_on: - wallet-health.yaml From 12eb1429ee35a28aa04eacb9209d7b2af93b9d74 Mon Sep 17 00:00:00 2001 From: Discostu Date: Wed, 6 May 2026 10:23:43 +0200 Subject: [PATCH 17/17] Revert "test(e2e): force english locale in qa fixtures" This reverts commit 15f9c708930ce764cc0acf670783a7fe56cf2779. --- e2e/fixtures/abstract-chain.yaml | 2 +- e2e/fixtures/asset-data-regression.yaml | 2 +- e2e/fixtures/bebop-solana-swap.yaml | 2 +- e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml | 2 +- e2e/fixtures/chainflip-lending-revamp-ui.yaml | 2 +- e2e/fixtures/chainflip-lending.yaml | 2 +- e2e/fixtures/eth-to-fox-swap.yaml | 2 +- e2e/fixtures/evm-chains-regression.yaml | 2 +- e2e/fixtures/fox-ecosystem.yaml | 2 +- e2e/fixtures/send-receive.yaml | 2 +- e2e/fixtures/stargate-usdc-eth-to-arb.yaml | 2 +- e2e/fixtures/swap-exploration.yaml | 2 +- e2e/fixtures/thorchain-solana-swapper.yaml | 2 +- e2e/fixtures/trade-exploration.yaml | 2 +- e2e/fixtures/tron-non-activated-balances.yaml | 2 +- e2e/fixtures/wallet-health.yaml | 2 +- e2e/fixtures/yield-e2e.yaml | 2 +- e2e/fixtures/yield-lending.yaml | 2 +- e2e/fixtures/yield-staking.yaml | 2 +- e2e/fixtures/yield-vault.yaml | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/e2e/fixtures/abstract-chain.yaml b/e2e/fixtures/abstract-chain.yaml index 5c97f09d76a..02a0b3cc599 100644 --- a/e2e/fixtures/abstract-chain.yaml +++ b/e2e/fixtures/abstract-chain.yaml @@ -1,6 +1,6 @@ name: Abstract Chain Integration description: Verify Abstract chain assets appear in swapper, search works, and ETH->Abstract swap quote can be obtained -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/asset-data-regression.yaml b/e2e/fixtures/asset-data-regression.yaml index d51628e3210..3e6b15dc3a3 100644 --- a/e2e/fixtures/asset-data-regression.yaml +++ b/e2e/fixtures/asset-data-regression.yaml @@ -4,7 +4,7 @@ description: > Tests USDC, USDT, and FOX search with grouped multi-chain variants via global search, plus markets recently added section. Does NOT require wallet connection - no depends_on smoke-test. -route: /trade?lang=en +route: /trade # ============================================================ # IMPLEMENTATION NOTES (for agent-browser execution): diff --git a/e2e/fixtures/bebop-solana-swap.yaml b/e2e/fixtures/bebop-solana-swap.yaml index 3b9085a75f6..7f963a1ad88 100644 --- a/e2e/fixtures/bebop-solana-swap.yaml +++ b/e2e/fixtures/bebop-solana-swap.yaml @@ -5,7 +5,7 @@ description: > THORChain LP tests the compute budget fix for memo transactions. Upstream Bebop errors (InsufficientLiquidity code 102, TransactionExecutionError code 208) are expected soft_fails, not our bugs. -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml b/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml index dc4935d5082..8e1c702969e 100644 --- a/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml +++ b/e2e/fixtures/chainflip-lending-action-center-pr-12064.yaml @@ -4,7 +4,7 @@ description: > already executed Chainflip lending USDC operations and the Action Center contains the resulting cards. Focuses only on notification center copy, expanded details, and egress transaction affordances. -route: /#/chainflip-lending/pool/eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48?lang=en +route: /#/chainflip-lending/pool/eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 steps: - name: Verify confirmed Chainflip lending cards instruction: > diff --git a/e2e/fixtures/chainflip-lending-revamp-ui.yaml b/e2e/fixtures/chainflip-lending-revamp-ui.yaml index 77a1b532fec..5b6ad6534af 100644 --- a/e2e/fixtures/chainflip-lending-revamp-ui.yaml +++ b/e2e/fixtures/chainflip-lending-revamp-ui.yaml @@ -1,6 +1,6 @@ name: Chainflip Lending Revamp UI description: Validates the revamped Chainflip lending dashboard, init view, funded view with sections, and key action modals. -route: /chainflip-lending?lang=en +route: /chainflip-lending steps: # === No-wallet state === - name: Landing page without wallet diff --git a/e2e/fixtures/chainflip-lending.yaml b/e2e/fixtures/chainflip-lending.yaml index 453523f6ea8..7e1740c38e9 100644 --- a/e2e/fixtures/chainflip-lending.yaml +++ b/e2e/fixtures/chainflip-lending.yaml @@ -78,7 +78,7 @@ description: > At each signing step, a "Confirm" button may appear for hardware wallets. Click it. If password prompt: enter $NATIVE_WALLET_PASSWORD. -route: /chainflip-lending?lang=en +route: /chainflip-lending depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/eth-to-fox-swap.yaml b/e2e/fixtures/eth-to-fox-swap.yaml index 0427789ba67..92a37a04ce3 100644 --- a/e2e/fixtures/eth-to-fox-swap.yaml +++ b/e2e/fixtures/eth-to-fox-swap.yaml @@ -1,6 +1,6 @@ name: ETH to FOX Swap description: Full ETH to FOX swap - select assets, enter amount, preview, sign, wait for completion -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/evm-chains-regression.yaml b/e2e/fixtures/evm-chains-regression.yaml index 68ee35fa341..2f707a8040f 100644 --- a/e2e/fixtures/evm-chains-regression.yaml +++ b/e2e/fixtures/evm-chains-regression.yaml @@ -4,7 +4,7 @@ description: > on each chain (native -> any token) and verifies the swap completes. Detects node failures, RPC issues, and swapper regressions. Chains: Ethereum, Base, Gnosis, Avalanche, Arbitrum, BSC, Optimism. -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/fox-ecosystem.yaml b/e2e/fixtures/fox-ecosystem.yaml index cf6fd6bc521..2b63b05ce83 100644 --- a/e2e/fixtures/fox-ecosystem.yaml +++ b/e2e/fixtures/fox-ecosystem.yaml @@ -1,6 +1,6 @@ name: fox-ecosystem description: Fox Ecosystem page - rFOX staking/unstaking/claiming, simulator, FOX token, farming, governance, action center notifications -route: /fox-ecosystem?lang=en +route: /fox-ecosystem depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/send-receive.yaml b/e2e/fixtures/send-receive.yaml index 67d202f07e5..b8776f57d9a 100644 --- a/e2e/fixtures/send-receive.yaml +++ b/e2e/fixtures/send-receive.yaml @@ -79,7 +79,7 @@ description: > - Total steps = wallet-health deps + (chains * steps_per_chain) - If a chain fails, log the failure and CONTINUE to the next chain. -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/stargate-usdc-eth-to-arb.yaml b/e2e/fixtures/stargate-usdc-eth-to-arb.yaml index 36e5d73fec7..fdd09368584 100644 --- a/e2e/fixtures/stargate-usdc-eth-to-arb.yaml +++ b/e2e/fixtures/stargate-usdc-eth-to-arb.yaml @@ -3,7 +3,7 @@ description: > Cross-chain USDC swap via Stargate V2 swapper: sell USDC on Ethereum mainnet, receive USDC on Arbitrum One. Verifies that Stargate appears as a quote source, that slippage is respected, and that the transaction builds and signs correctly. -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/swap-exploration.yaml b/e2e/fixtures/swap-exploration.yaml index 8a03b370e29..f5939b91943 100644 --- a/e2e/fixtures/swap-exploration.yaml +++ b/e2e/fixtures/swap-exploration.yaml @@ -40,7 +40,7 @@ description: > - Track which pairs worked and which failed mode: exploratory -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/thorchain-solana-swapper.yaml b/e2e/fixtures/thorchain-solana-swapper.yaml index 86eff781d9b..27b6b5a9b13 100644 --- a/e2e/fixtures/thorchain-solana-swapper.yaml +++ b/e2e/fixtures/thorchain-solana-swapper.yaml @@ -1,6 +1,6 @@ name: THORChain Solana Swapper description: Test THORChain Solana swapper integration - SOL to RUNE and RUNE to SOL swaps, specifically selecting THORChain quotes -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/trade-exploration.yaml b/e2e/fixtures/trade-exploration.yaml index afaf125c847..2e57c0f3976 100644 --- a/e2e/fixtures/trade-exploration.yaml +++ b/e2e/fixtures/trade-exploration.yaml @@ -9,7 +9,7 @@ description: > "Found: infinite loading when switching assets rapidly" mode: exploratory -route: /trade?lang=en +route: /trade depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/tron-non-activated-balances.yaml b/e2e/fixtures/tron-non-activated-balances.yaml index f985191e439..63aeda9ae76 100644 --- a/e2e/fixtures/tron-non-activated-balances.yaml +++ b/e2e/fixtures/tron-non-activated-balances.yaml @@ -2,7 +2,7 @@ name: Tron Non-Activated Account Balances description: > Verifies fix for #12190 - USDT/TRC20 tokens show in portfolio for non-activated Tron accounts, and send flow shows a warning when sending TRC20 to a non-activated recipient address. -route: /?lang=en +route: / depends_on: - wallet-health.yaml steps: diff --git a/e2e/fixtures/wallet-health.yaml b/e2e/fixtures/wallet-health.yaml index db3e1624d42..31b3d8eb41c 100644 --- a/e2e/fixtures/wallet-health.yaml +++ b/e2e/fixtures/wallet-health.yaml @@ -1,6 +1,6 @@ name: Wallet Health description: Dismiss onboarding, unlock native wallet, verify trade page loads with swap inputs and button -route: /trade?lang=en +route: /trade steps: - name: Dismiss onboarding and banners instruction: > diff --git a/e2e/fixtures/yield-e2e.yaml b/e2e/fixtures/yield-e2e.yaml index f525ede4132..c832567f5b4 100644 --- a/e2e/fixtures/yield-e2e.yaml +++ b/e2e/fixtures/yield-e2e.yaml @@ -165,7 +165,7 @@ description: > Compare APY, TVL, provider name, asset name against what the UI shows. Any mismatch is a SOFT FAIL bug worth reporting. -route: /earn?lang=en +route: /earn depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/yield-lending.yaml b/e2e/fixtures/yield-lending.yaml index 174d34f0d1c..646935f7091 100644 --- a/e2e/fixtures/yield-lending.yaml +++ b/e2e/fixtures/yield-lending.yaml @@ -66,7 +66,7 @@ description: > 4. Preview, confirm, sign, wait 120s. Navigate back. 5. Report: "FUNDING SWAP: $0.20 {source} → {target}" -route: /earn?lang=en +route: /earn depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/yield-staking.yaml b/e2e/fixtures/yield-staking.yaml index 28a6019a154..d3848fb852d 100644 --- a/e2e/fixtures/yield-staking.yaml +++ b/e2e/fixtures/yield-staking.yaml @@ -63,7 +63,7 @@ description: > - window.location.hash for navigation - Native wallet signs automatically -route: /earn?lang=en +route: /earn depends_on: - wallet-health.yaml diff --git a/e2e/fixtures/yield-vault.yaml b/e2e/fixtures/yield-vault.yaml index dbe23d1ed17..2d26a7faa26 100644 --- a/e2e/fixtures/yield-vault.yaml +++ b/e2e/fixtures/yield-vault.yaml @@ -52,7 +52,7 @@ description: > 3. FIAT $0.20 (enough for a yield deposit). Max $3. 4. Preview, confirm, sign, wait 120s. Navigate back. -route: /earn?lang=en +route: /earn depends_on: - wallet-health.yaml