)
diff --git a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/swap/loading.tsx b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/swap/loading.tsx
index 97f11a2e68..7c547392cd 100644
--- a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/swap/loading.tsx
+++ b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/swap/loading.tsx
@@ -7,20 +7,20 @@ export default function SimpleSwapLoading() {
-
-
+
+
-
-
-
-
+
+
+
+
-
)
diff --git a/apps/web/src/app/(networks)/(evm)/api/cross-chain/route.ts b/apps/web/src/app/(networks)/(evm)/api/cross-chain/route.ts
deleted file mode 100644
index d11ea2ab7c..0000000000
--- a/apps/web/src/app/(networks)/(evm)/api/cross-chain/route.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { NextRequest } from 'next/server'
-import { SushiXSwap2Adapter } from 'src/lib/swap/cross-chain'
-import { getCrossChainTrade } from 'src/lib/swap/cross-chain/actions/getCrossChainTrade'
-import { getCrossChainTrades } from 'src/lib/swap/cross-chain/actions/getCrossChainTrades'
-import { ChainId } from 'sushi/chain'
-import { SushiXSwap2ChainId, isSushiXSwap2ChainId } from 'sushi/config'
-import { getAddress } from 'viem'
-import { z } from 'zod'
-
-const schema = z.object({
- adapter: z.optional(z.nativeEnum(SushiXSwap2Adapter)),
- srcChainId: z.coerce
- .number()
- .refine((chainId) => isSushiXSwap2ChainId(chainId as ChainId), {
- message: `srchChainId must exist in SushiXSwapV2ChainId`,
- })
- .transform((chainId) => chainId as SushiXSwap2ChainId),
- dstChainId: z.coerce
- .number()
- .refine((chainId) => isSushiXSwap2ChainId(chainId as ChainId), {
- message: `dstChainId must exist in SushiXSwapV2ChainId`,
- })
- .transform((chainId) => chainId as SushiXSwap2ChainId),
- tokenIn: z.string().transform((token) => getAddress(token)),
- tokenOut: z.string().transform((token) => getAddress(token)),
- amount: z.string().transform((amount) => BigInt(amount)),
- srcGasPrice: z.optional(
- z.coerce
- .number()
- .int('gasPrice should be integer')
- .gt(0, 'gasPrice should be positive')
- .transform((gasPrice) => BigInt(gasPrice)),
- ),
- dstGasPrice: z.optional(
- z.coerce
- .number()
- .int('gasPrice should be integer')
- .gt(0, 'gasPrice should be positive')
- .transform((gasPrice) => BigInt(gasPrice)),
- ),
- from: z
- .optional(z.string())
- .transform((from) => (from ? getAddress(from) : undefined)),
- recipient: z
- .optional(z.string())
- .transform((to) => (to ? getAddress(to) : undefined)),
- preferSushi: z.optional(z.coerce.boolean()),
- maxSlippage: z.coerce
- .number()
- .lt(1, 'maxPriceImpact should be lesser than 1')
- .gt(0, 'maxPriceImpact should be positive'),
-})
-
-export const revalidate = 600
-
-export async function GET(request: NextRequest) {
- const params = Object.fromEntries(request.nextUrl.searchParams.entries())
-
- const { adapter, ...parsedParams } = schema.parse(params)
-
- const getCrossChainTradeParams = {
- ...parsedParams,
- slippagePercentage: (parsedParams.maxSlippage * 100).toString(),
- }
-
- const crossChainSwap = await (typeof adapter === 'undefined'
- ? getCrossChainTrades(getCrossChainTradeParams)
- : getCrossChainTrade({ adapter, ...getCrossChainTradeParams }))
-
- return Response.json(crossChainSwap, {
- headers: {
- 'Cache-Control': 'max-age=60, stale-while-revalidate=600',
- },
- })
-}
diff --git a/apps/web/src/app/(networks)/(evm)/api/cross-chain/routes/route.ts b/apps/web/src/app/(networks)/(evm)/api/cross-chain/routes/route.ts
new file mode 100644
index 0000000000..7198b19dd3
--- /dev/null
+++ b/apps/web/src/app/(networks)/(evm)/api/cross-chain/routes/route.ts
@@ -0,0 +1,80 @@
+import { NextRequest } from 'next/server'
+import { isXSwapSupportedChainId } from 'src/config'
+import { isAddress } from 'viem'
+import { z } from 'zod'
+
+const schema = z.object({
+ fromChainId: z.coerce
+ .number()
+ .refine((chainId) => isXSwapSupportedChainId(chainId), {
+ message: `fromChainId must exist in XSwapChainId`,
+ }),
+ fromAmount: z.string(),
+ fromTokenAddress: z.string().refine((token) => isAddress(token), {
+ message: 'fromTokenAddress does not conform to Address',
+ }),
+ toChainId: z.coerce
+ .number()
+ .refine((chainId) => isXSwapSupportedChainId(chainId), {
+ message: `toChainId must exist in XSwapChainId`,
+ }),
+ toTokenAddress: z.string().refine((token) => isAddress(token), {
+ message: 'toTokenAddress does not conform to Address',
+ }),
+ fromAddress: z
+ .string()
+ .refine((address) => isAddress(address), {
+ message: 'fromAddress does not conform to Address',
+ })
+ .optional(),
+ toAddress: z
+ .string()
+ .refine((address) => isAddress(address), {
+ message: 'toAddress does not conform to Address',
+ })
+ .optional(),
+ slippage: z.coerce.number(), // decimal
+ order: z.enum(['CHEAPEST', 'FASTEST']).optional(),
+})
+
+export const revalidate = 20
+
+export async function GET(request: NextRequest) {
+ const params = Object.fromEntries(request.nextUrl.searchParams.entries())
+
+ const { slippage, order = 'CHEAPEST', ...parsedParams } = schema.parse(params)
+
+ const url = new URL('https://li.quest/v1/advanced/routes')
+
+ const options = {
+ method: 'POST',
+ headers: {
+ accept: 'application/json',
+ 'content-type': 'application/json',
+ ...(process.env.LIFI_API_KEY && {
+ 'x-lifi-api-key': process.env.LIFI_API_KEY,
+ }),
+ },
+ body: JSON.stringify({
+ ...parsedParams,
+ options: {
+ slippage,
+ order,
+ integrator: 'sushi',
+ exchanges: { allow: ['sushiswap'] },
+ allowSwitchChain: false,
+ allowDestinationCall: true,
+ // fee: // TODO: must set up feeReceiver w/ lifi
+ },
+ }),
+ }
+
+ const response = await fetch(url, options)
+
+ return Response.json(await response.json(), {
+ status: response.status,
+ headers: {
+ 'Cache-Control': 's-maxage=15, stale-while-revalidate=20',
+ },
+ })
+}
diff --git a/apps/web/src/app/(networks)/(evm)/api/cross-chain/step/route.ts b/apps/web/src/app/(networks)/(evm)/api/cross-chain/step/route.ts
new file mode 100644
index 0000000000..8eb9cf4ec4
--- /dev/null
+++ b/apps/web/src/app/(networks)/(evm)/api/cross-chain/step/route.ts
@@ -0,0 +1,50 @@
+import { NextRequest } from 'next/server'
+import {
+ crossChainActionSchema,
+ crossChainStepSchema,
+} from 'src/lib/swap/cross-chain/schema'
+import { isAddress, stringify } from 'viem'
+import { z } from 'zod'
+
+const schema = crossChainStepSchema.extend({
+ action: crossChainActionSchema.extend({
+ fromAddress: z.string().refine((address) => isAddress(address), {
+ message: 'fromAddress does not conform to Address',
+ }),
+ toAddress: z.string().refine((address) => isAddress(address), {
+ message: 'toAddress does not conform to Address',
+ }),
+ }),
+})
+
+export async function POST(request: NextRequest) {
+ const params = await request.json()
+
+ const parsedParams = schema.parse(params)
+
+ const url = new URL('https://li.quest/v1/advanced/stepTransaction')
+
+ const options = {
+ method: 'POST',
+ headers: {
+ accept: 'application/json',
+ 'content-type': 'application/json',
+ ...(process.env.LIFI_API_KEY && {
+ 'x-lifi-api-key': process.env.LIFI_API_KEY,
+ }),
+ },
+ body: stringify({
+ ...parsedParams,
+ integrator: 'sushi',
+ }),
+ }
+
+ const response = await fetch(url, options)
+
+ return Response.json(await response.json(), {
+ status: response.status,
+ headers: {
+ 'Cache-Control': 's-maxage=8, stale-while-revalidate=10',
+ },
+ })
+}
diff --git a/apps/web/src/config.ts b/apps/web/src/config.ts
index 122729fb5a..a88cf8ab62 100644
--- a/apps/web/src/config.ts
+++ b/apps/web/src/config.ts
@@ -112,6 +112,19 @@ export const PREFERRED_CHAINID_ORDER = [
ChainId.BOBA_BNB,
] as const
+export const getSortedChainIds =
(
+ chainIds: readonly T[],
+) => {
+ return Array.from(
+ new Set([
+ ...(PREFERRED_CHAINID_ORDER.filter((el) =>
+ chainIds.includes(el as (typeof chainIds)[number]),
+ ) as T[]),
+ ...chainIds,
+ ]),
+ )
+}
+
export const CHAIN_IDS = [
...SUSHISWAP_SUPPORTED_CHAIN_IDS,
...AGGREGATOR_ONLY_CHAIN_IDS,
@@ -218,3 +231,37 @@ export const isZapSupportedChainId = (
chainId: number,
): chainId is ZapSupportedChainId =>
ZAP_SUPPORTED_CHAIN_IDS.includes(chainId as ZapSupportedChainId)
+
+export const XSWAP_SUPPORTED_CHAIN_IDS = [
+ ChainId.ARBITRUM,
+ ChainId.AVALANCHE,
+ ChainId.BSC,
+ ChainId.BASE,
+ ChainId.BLAST,
+ ChainId.BOBA,
+ ChainId.CELO,
+ ChainId.CRONOS,
+ ChainId.ETHEREUM,
+ ChainId.FUSE,
+ ChainId.FANTOM,
+ ChainId.GNOSIS,
+ ChainId.LINEA,
+ ChainId.MANTLE,
+ ChainId.METIS,
+ ChainId.MODE,
+ ChainId.MOONBEAM,
+ ChainId.MOONRIVER,
+ ChainId.OPTIMISM,
+ ChainId.POLYGON,
+ ChainId.POLYGON_ZKEVM,
+ ChainId.ROOTSTOCK,
+ ChainId.SCROLL,
+ ChainId.TAIKO,
+ ChainId.ZKSYNC_ERA,
+] as const
+
+export type XSwapSupportedChainId = (typeof XSWAP_SUPPORTED_CHAIN_IDS)[number]
+export const isXSwapSupportedChainId = (
+ chainId: number,
+): chainId is XSwapSupportedChainId =>
+ XSWAP_SUPPORTED_CHAIN_IDS.includes(chainId as XSwapSupportedChainId)
diff --git a/apps/web/src/lib/hooks/api/index.ts b/apps/web/src/lib/hooks/api/index.ts
index e6275b95a7..0b301d7b77 100644
--- a/apps/web/src/lib/hooks/api/index.ts
+++ b/apps/web/src/lib/hooks/api/index.ts
@@ -1,5 +1,4 @@
export * from './useApprovedCommunityTokens'
-export * from './useCrossChainTrade'
export * from './usePoolGraphData'
export * from './usePoolsInfinite'
export * from './userSmartPools'
diff --git a/apps/web/src/lib/hooks/api/useCrossChainTrade.ts b/apps/web/src/lib/hooks/api/useCrossChainTrade.ts
deleted file mode 100644
index 94b368fffc..0000000000
--- a/apps/web/src/lib/hooks/api/useCrossChainTrade.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-import { UseQueryOptions, useQuery } from '@tanstack/react-query'
-import { NativeAddress } from 'src/lib/constants'
-import { apiAdapter02To01 } from 'src/lib/hooks/react-query'
-import {
- CrossChainTradeSchema,
- GetCrossChainTradeParams,
- SushiXSwap2Adapter,
- SushiXSwapFunctionName,
- SushiXSwapTransactionType,
- SushiXSwapWriteArgs,
-} from 'src/lib/swap/cross-chain'
-import { Amount, Native, Token, Type } from 'sushi/currency'
-import { Percent } from 'sushi/math'
-import { RouteStatus } from 'sushi/router'
-import { stringify } from 'viem'
-
-export interface UseCrossChainTradeReturn {
- status: RouteStatus
- adapter: SushiXSwap2Adapter
- tokenIn: Type
- tokenOut: Type
- srcBridgeToken?: Type
- dstBridgeToken?: Type
- amountIn?: Amount
- amountOut?: Amount
- amountOutMin?: Amount
- priceImpact?: Percent
- srcTrade?: ReturnType
- dstTrade?: ReturnType
- transactionType?: SushiXSwapTransactionType
- gasSpent?: string
- bridgeFee?: string
- srcGasFee?: string
- functionName?: SushiXSwapFunctionName
- writeArgs?: SushiXSwapWriteArgs
- value?: string
-}
-
-export interface UseCrossChainTradeParms
- extends Omit<
- GetCrossChainTradeParams,
- 'tokenIn' | 'tokenOut' | 'amount' | 'gasSpent'
- > {
- adapter?: SushiXSwap2Adapter
- tokenIn?: Type
- tokenOut?: Type
- amount?: Amount
- query?: Omit<
- UseQueryOptions,
- 'queryFn' | 'queryKey'
- >
-}
-
-export const useCrossChainTrade = ({
- query,
- ...params
-}: UseCrossChainTradeParms) => {
- const { tokenIn, tokenOut, amount, slippagePercentage, ...rest } = params
-
- return useQuery({
- ...query,
- queryKey: ['cross-chain', params],
- queryFn: async (): Promise => {
- if (!tokenIn || !tokenOut || !amount) throw new Error()
-
- const url = new URL('/api/cross-chain', window.location.origin)
-
- url.searchParams.set(
- 'tokenIn',
- tokenIn.isNative ? NativeAddress : tokenIn.address,
- )
- url.searchParams.set(
- 'tokenOut',
- tokenOut.isNative ? NativeAddress : tokenOut.address,
- )
- url.searchParams.set('amount', amount.quotient.toString())
- url.searchParams.set('maxSlippage', `${+slippagePercentage / 100}`)
-
- Object.entries(rest).forEach(([key, value]) => {
- value && url.searchParams.set(key, value.toString())
- })
-
- const res = await fetch(url.toString())
-
- const json = await res.json()
-
- const parsed = CrossChainTradeSchema.parse(json)
-
- const { status, adapter } = parsed
-
- if (status === RouteStatus.NoWay)
- return {
- status,
- adapter,
- tokenIn,
- tokenOut,
- }
-
- const srcBridgeToken = parsed.srcBridgeToken.isNative
- ? Native.deserialize(parsed.srcBridgeToken)
- : Token.deserialize(parsed.srcBridgeToken)
-
- const dstBridgeToken = parsed.dstBridgeToken.isNative
- ? Native.deserialize(parsed.dstBridgeToken)
- : Token.deserialize(parsed.dstBridgeToken)
-
- const srcTrade = parsed.srcTrade
- ? apiAdapter02To01(
- parsed.srcTrade,
- tokenIn,
- srcBridgeToken,
- parsed.srcTrade?.status !== RouteStatus.NoWay
- ? parsed.srcTrade?.routeProcessorArgs?.to
- : undefined,
- )
- : undefined
-
- const dstTrade = parsed.dstTrade
- ? apiAdapter02To01(
- parsed.dstTrade,
- dstBridgeToken,
- tokenOut,
- parsed.dstTrade.status !== RouteStatus.NoWay
- ? parsed.dstTrade.routeProcessorArgs?.to
- : undefined,
- )
- : undefined
-
- return {
- ...parsed,
- tokenIn,
- tokenOut,
- srcBridgeToken,
- dstBridgeToken,
- srcTrade,
- dstTrade,
- amountIn: Amount.fromRawAmount(tokenIn, parsed.amountIn),
- amountOut: Amount.fromRawAmount(tokenOut, parsed.amountOut),
- amountOutMin: Amount.fromRawAmount(tokenOut, parsed.amountOutMin),
- priceImpact: new Percent(Math.round(parsed.priceImpact * 10000), 10000),
- gasSpent: parsed.gasSpent
- ? Amount.fromRawAmount(
- Native.onChain(tokenIn.chainId),
- parsed.gasSpent,
- ).toFixed(6)
- : undefined,
- bridgeFee: parsed.bridgeFee
- ? Amount.fromRawAmount(
- Native.onChain(tokenIn.chainId),
- parsed.bridgeFee,
- ).toFixed(6)
- : undefined,
- srcGasFee: parsed.srcGasFee
- ? Amount.fromRawAmount(
- Native.onChain(tokenIn.chainId),
- parsed.srcGasFee,
- ).toFixed(6)
- : undefined,
- }
- },
- enabled: query?.enabled !== false && Boolean(tokenIn && tokenOut && amount),
- queryKeyHashFn: stringify,
- })
-}
diff --git a/apps/web/src/lib/hooks/react-query/cross-chain-trade/index.ts b/apps/web/src/lib/hooks/react-query/cross-chain-trade/index.ts
new file mode 100644
index 0000000000..e200dd2f85
--- /dev/null
+++ b/apps/web/src/lib/hooks/react-query/cross-chain-trade/index.ts
@@ -0,0 +1,2 @@
+export * from './useCrossChainTradeRoutes'
+export * from './useCrossChainTradeStep'
diff --git a/apps/web/src/lib/hooks/react-query/cross-chain-trade/types.ts b/apps/web/src/lib/hooks/react-query/cross-chain-trade/types.ts
new file mode 100644
index 0000000000..09c921de92
--- /dev/null
+++ b/apps/web/src/lib/hooks/react-query/cross-chain-trade/types.ts
@@ -0,0 +1,15 @@
+import { z } from 'zod'
+import {
+ crossChainActionSchema,
+ crossChainRouteSchema,
+ crossChainStepSchema,
+ crossChainToolDetailsSchema,
+} from '../../../swap/cross-chain/schema'
+
+export type CrossChainAction = z.infer
+
+export type CrossChainRoute = z.infer
+
+export type CrossChainStep = z.infer
+
+export type CrossChainToolDetails = z.infer
diff --git a/apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeRoutes.ts b/apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeRoutes.ts
new file mode 100644
index 0000000000..6eee8c965b
--- /dev/null
+++ b/apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeRoutes.ts
@@ -0,0 +1,81 @@
+import { UseQueryOptions, useQuery } from '@tanstack/react-query'
+import { Amount, Type } from 'sushi/currency'
+import { Percent } from 'sushi/math'
+import { Address, zeroAddress } from 'viem'
+import { z } from 'zod'
+import { crossChainRouteSchema } from '../../../swap/cross-chain/schema'
+import { CrossChainRoute } from '../../../swap/cross-chain/types'
+
+const crossChainRoutesResponseSchema = z.object({
+ routes: z.array(crossChainRouteSchema),
+})
+
+export interface UseCrossChainTradeRoutesParms {
+ fromAmount?: Amount
+ toToken?: Type
+ fromAddress?: Address
+ toAddress?: Address
+ slippage: Percent
+ order?: 'CHEAPEST' | 'FASTEST'
+ query?: Omit, 'queryFn' | 'queryKey'>
+}
+
+export const useCrossChainTradeRoutes = ({
+ query,
+ ...params
+}: UseCrossChainTradeRoutesParms) => {
+ return useQuery({
+ queryKey: ['cross-chain/routes', params],
+ queryFn: async (): Promise => {
+ const { fromAmount, toToken, slippage } = params
+
+ if (!fromAmount || !toToken) throw new Error()
+
+ const url = new URL('/api/cross-chain/routes', window.location.origin)
+
+ url.searchParams.set(
+ 'fromChainId',
+ fromAmount.currency.chainId.toString(),
+ )
+ url.searchParams.set('toChainId', toToken.chainId.toString())
+ url.searchParams.set(
+ 'fromTokenAddress',
+ fromAmount.currency.isNative
+ ? zeroAddress
+ : fromAmount.currency.address,
+ )
+ url.searchParams.set(
+ 'toTokenAddress',
+ toToken.isNative ? zeroAddress : toToken.address,
+ )
+ url.searchParams.set('fromAmount', fromAmount.quotient.toString())
+ url.searchParams.set('slippage', `${+slippage.toFixed(2) / 100}`)
+ params.fromAddress &&
+ url.searchParams.set('fromAddress', params.fromAddress)
+ params.toAddress ||
+ (params.fromAddress &&
+ url.searchParams.set(
+ 'toAddress',
+ params.toAddress || params.fromAddress,
+ ))
+ params.order && url.searchParams.set('order', params.order)
+
+ const response = await fetch(url)
+
+ if (!response.ok) {
+ throw new Error(response.statusText)
+ }
+
+ const json = await response.json()
+
+ const { routes } = crossChainRoutesResponseSchema.parse(json)
+
+ return routes
+ },
+ refetchInterval: query?.refetchInterval ?? 1000 * 20, // 20s
+ enabled:
+ query?.enabled !== false &&
+ Boolean(params.toToken && params.fromAmount?.greaterThan(0)),
+ ...query,
+ })
+}
diff --git a/apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeStep.ts b/apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeStep.ts
new file mode 100644
index 0000000000..fe31038cff
--- /dev/null
+++ b/apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeStep.ts
@@ -0,0 +1,110 @@
+import { UseQueryOptions, useQuery } from '@tanstack/react-query'
+import { Amount, Native, Token, Type } from 'sushi/currency'
+import { Percent } from 'sushi/math'
+import { zeroAddress } from 'viem'
+import { stringify } from 'viem/utils'
+import { crossChainStepSchema } from '../../../swap/cross-chain/schema'
+import { CrossChainStep } from '../../../swap/cross-chain/types'
+
+export interface UseCrossChainTradeStepReturn extends CrossChainStep {
+ tokenIn: Type
+ tokenOut: Type
+ amountIn?: Amount
+ amountOut?: Amount
+ amountOutMin?: Amount
+ priceImpact?: Percent
+}
+
+export interface UseCrossChainTradeStepParms {
+ step: CrossChainStep | undefined
+ query?: Omit<
+ UseQueryOptions,
+ 'queryFn' | 'queryKey' | 'queryKeyFn'
+ >
+}
+
+export const useCrossChainTradeStep = ({
+ query,
+ ...params
+}: UseCrossChainTradeStepParms) => {
+ return useQuery({
+ queryKey: ['cross-chain/step', params],
+ queryFn: async () => {
+ const { step } = params
+
+ if (!step) throw new Error()
+
+ const url = new URL('/api/cross-chain/step', window.location.origin)
+
+ const options = {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: stringify(step),
+ }
+
+ const response = await fetch(url, options)
+
+ if (!response.ok) {
+ throw new Error(response.statusText)
+ }
+
+ const json = await response.json()
+
+ const parsedStep = crossChainStepSchema.parse(json)
+
+ const tokenIn =
+ parsedStep.action.fromToken.address === zeroAddress
+ ? Native.onChain(parsedStep.action.fromToken.chainId)
+ : new Token(parsedStep.action.fromToken)
+
+ const tokenOut =
+ parsedStep.action.toToken.address === zeroAddress
+ ? Native.onChain(parsedStep.action.toToken.chainId)
+ : new Token(parsedStep.action.toToken)
+
+ const amountIn = Amount.fromRawAmount(
+ tokenIn,
+ parsedStep.action.fromAmount,
+ )
+ const amountOut = Amount.fromRawAmount(
+ tokenOut,
+ parsedStep.estimate.toAmount,
+ )
+ const amountOutMin = Amount.fromRawAmount(
+ tokenOut,
+ parsedStep.estimate.toAmountMin,
+ )
+
+ const fromAmountUSD =
+ (Number(parsedStep.action.fromToken.priceUSD) *
+ Number(amountIn.quotient)) /
+ 10 ** tokenIn.decimals
+
+ const toAmountUSD =
+ (Number(parsedStep.action.toToken.priceUSD) *
+ Number(amountOut.quotient)) /
+ 10 ** tokenOut.decimals
+
+ const priceImpact = new Percent(
+ Math.floor((fromAmountUSD / toAmountUSD - 1) * 10_000),
+ 10_000,
+ )
+
+ return {
+ ...parsedStep,
+ tokenIn,
+ tokenOut,
+ amountIn,
+ amountOut,
+ amountOutMin,
+ priceImpact,
+ }
+ },
+ refetchInterval: query?.refetchInterval ?? 1000 * 10, // 10s
+ enabled: query?.enabled !== false && Boolean(params.step),
+ queryKeyHashFn: stringify,
+ ...query,
+ })
+}
diff --git a/apps/web/src/lib/hooks/react-query/index.ts b/apps/web/src/lib/hooks/react-query/index.ts
index 80de98a612..ece2de67b6 100644
--- a/apps/web/src/lib/hooks/react-query/index.ts
+++ b/apps/web/src/lib/hooks/react-query/index.ts
@@ -1,3 +1,4 @@
+export * from './cross-chain-trade'
export * from './pools'
export * from './prices'
export * from './rewards'
diff --git a/apps/web/src/lib/swap/cross-chain/actions/getCrossChainTrade.ts b/apps/web/src/lib/swap/cross-chain/actions/getCrossChainTrade.ts
deleted file mode 100644
index f3c5670d6b..0000000000
--- a/apps/web/src/lib/swap/cross-chain/actions/getCrossChainTrade.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { tradeValidator02 } from 'src/lib/hooks/react-query'
-import { SushiXSwap2ChainId } from 'sushi/config'
-import { RouteStatus } from 'sushi/router'
-import { Address } from 'viem'
-import { z } from 'zod'
-import {
- SushiXSwap2Adapter,
- SushiXSwapFunctionName,
- SushiXSwapTransactionType,
-} from '../lib'
-import { getSquidCrossChainTrade } from './getSquidCrossChainTrade'
-import { getStargateCrossChainTrade } from './getStargateCrossChainTrade'
-
-export interface GetCrossChainTradeParams {
- srcChainId: SushiXSwap2ChainId
- dstChainId: SushiXSwap2ChainId
- tokenIn: Address
- tokenOut: Address
- amount: bigint
- srcGasPrice?: bigint
- dstGasPrice?: bigint
- slippagePercentage: string
- from?: Address
- recipient?: Address
-}
-
-const currencyValidator = z.union([
- z.object({
- isNative: z.literal(true),
- name: z.optional(z.string()),
- symbol: z.optional(z.string()),
- decimals: z.number(),
- chainId: z.number(),
- }),
- z.object({
- isNative: z.literal(false),
- name: z.optional(z.string()),
- symbol: z.optional(z.string()),
- address: z.string(),
- decimals: z.number(),
- chainId: z.number(),
- }),
-])
-
-const CrossChainTradeNotFoundSchema = z.object({
- adapter: z.nativeEnum(SushiXSwap2Adapter),
- status: z.enum([RouteStatus.NoWay]),
-})
-
-const CrossChainTradeFoundSchema = z.object({
- status: z.enum([RouteStatus.Success, RouteStatus.Partial]),
- adapter: z.nativeEnum(SushiXSwap2Adapter),
- tokenIn: z.string(),
- tokenOut: z.string(),
- srcBridgeToken: currencyValidator,
- dstBridgeToken: currencyValidator,
- amountIn: z.string(),
- amountOut: z.string(),
- amountOutMin: z.string(),
- priceImpact: z.number(),
- srcTrade: z.optional(tradeValidator02),
- dstTrade: z.optional(tradeValidator02),
- transactionType: z.optional(z.nativeEnum(SushiXSwapTransactionType)),
- gasSpent: z.optional(z.string()),
- bridgeFee: z.optional(z.string()),
- srcGasFee: z.optional(z.string()),
- functionName: z.optional(z.nativeEnum(SushiXSwapFunctionName)),
- writeArgs: z.optional(
- z.array(z.union([z.string(), z.object({}).passthrough()])),
- ),
- value: z.optional(z.string()),
-})
-
-export const CrossChainTradeSchema = z.union([
- CrossChainTradeNotFoundSchema,
- CrossChainTradeFoundSchema,
-])
-
-export type CrossChainTradeSchemaType = z.infer
-
-export const getCrossChainTrade = async ({
- adapter,
- ...params
-}: GetCrossChainTradeParams & { adapter: SushiXSwap2Adapter }) => {
- switch (adapter) {
- case SushiXSwap2Adapter.Squid:
- return getSquidCrossChainTrade(params)
- case SushiXSwap2Adapter.Stargate:
- return getStargateCrossChainTrade(params)
- }
-}
diff --git a/apps/web/src/lib/swap/cross-chain/actions/getCrossChainTrades.ts b/apps/web/src/lib/swap/cross-chain/actions/getCrossChainTrades.ts
deleted file mode 100644
index cfbeb45d5c..0000000000
--- a/apps/web/src/lib/swap/cross-chain/actions/getCrossChainTrades.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { GetCrossChainTradeParams } from './getCrossChainTrade'
-import { getSquidCrossChainTrade } from './getSquidCrossChainTrade'
-import { getStargateCrossChainTrade } from './getStargateCrossChainTrade'
-
-export const getCrossChainTrades = async (params: GetCrossChainTradeParams) => {
- return (
- await Promise.all([
- getSquidCrossChainTrade(params),
- getStargateCrossChainTrade(params),
- ])
- ).filter((resp) => !!resp)
-}
diff --git a/apps/web/src/lib/swap/cross-chain/actions/getSquidCrossChainTrade.ts b/apps/web/src/lib/swap/cross-chain/actions/getSquidCrossChainTrade.ts
deleted file mode 100644
index 1a7274a260..0000000000
--- a/apps/web/src/lib/swap/cross-chain/actions/getSquidCrossChainTrade.ts
+++ /dev/null
@@ -1,373 +0,0 @@
-import {
- ChainType,
- DexName,
- Hook,
- RouteRequest,
- SquidCallType,
-} from '@0xsquid/squid-types'
-import { NativeAddress } from 'src/lib/constants'
-import { routeProcessor4Abi_processRoute, squidRouterAbi } from 'sushi/abi'
-import {
- ROUTE_PROCESSOR_4_ADDRESS,
- SQUID_ADAPTER_ADDRESS,
- SQUID_ROUTER_ADDRESS,
- isSquidAdapterChainId,
-} from 'sushi/config'
-import { axlUSDC } from 'sushi/currency'
-import { RouteStatus, RouterLiquiditySource } from 'sushi/router'
-import { Address, Hex, encodeFunctionData, erc20Abi, zeroAddress } from 'viem'
-import {
- SushiXSwap2Adapter,
- SushiXSwapFunctionName,
- SushiXSwapTransactionType,
- applySlippage,
- decodeSquidRouterCallData,
- encodeRouteProcessorArgs,
- encodeSquidBridgeParams,
- getSquidTrade,
- isSquidRouteProcessorEnabled,
-} from '../lib'
-import {
- CrossChainTradeSchemaType,
- GetCrossChainTradeParams,
-} from './getCrossChainTrade'
-import { getSquidRoute } from './getSquidRoute'
-import {
- SuccessfulTradeReturn,
- getTrade,
- isSuccessfulTradeReturn,
-} from './getTrade'
-
-export const getSquidCrossChainTrade = async ({
- srcChainId,
- dstChainId,
- tokenIn,
- tokenOut,
- amount,
- slippagePercentage,
- srcGasPrice,
- dstGasPrice,
- from,
- recipient,
-}: GetCrossChainTradeParams): Promise => {
- try {
- const bridgePath =
- isSquidAdapterChainId(srcChainId) && isSquidAdapterChainId(dstChainId)
- ? {
- srcBridgeToken: axlUSDC[srcChainId],
- dstBridgeToken: axlUSDC[dstChainId],
- }
- : undefined
-
- if (!bridgePath) {
- throw new Error('getSquidCrossChainTrade: no bridge route found')
- }
- const { srcBridgeToken, dstBridgeToken } = bridgePath
-
- // has swap on source chain
- const isSrcSwap = Boolean(
- tokenIn.toLowerCase() !== srcBridgeToken.address.toLowerCase(),
- )
-
- // has swap on destination chain
- const isDstSwap = Boolean(
- tokenOut.toLowerCase() !== dstBridgeToken.address.toLowerCase(),
- )
-
- // whether to use RP for routing, uses to Squid when
- // no liquidity through RP-compatible pools
- const useRPOnSrc = Boolean(
- isSrcSwap && isSquidRouteProcessorEnabled[srcChainId],
- )
- const useRPOnDst = Boolean(
- isDstSwap &&
- isSquidRouteProcessorEnabled[dstChainId] &&
- Boolean(isSrcSwap ? useRPOnSrc : true),
- )
-
- const _srcRPTrade = useRPOnSrc
- ? await getTrade({
- chainId: srcChainId,
- amount,
- fromToken: tokenIn,
- toToken: srcBridgeToken.address,
- slippagePercentage,
- gasPrice: srcGasPrice,
- recipient: SQUID_ROUTER_ADDRESS[srcChainId],
- source: RouterLiquiditySource.XSwap,
- })
- : undefined
-
- if (useRPOnSrc && !isSuccessfulTradeReturn(_srcRPTrade!)) {
- throw new Error('getSquidCrossChainTrade: srcRPTrade failed')
- }
-
- const srcRPTrade = useRPOnSrc
- ? (_srcRPTrade as SuccessfulTradeReturn)
- : undefined
-
- const dstAmountIn = useRPOnSrc
- ? BigInt(srcRPTrade!.assumedAmountOut)
- : amount
-
- const _dstRPTrade = useRPOnDst
- ? await getTrade({
- chainId: dstChainId,
- amount: dstAmountIn,
- fromToken: dstBridgeToken.address,
- toToken: tokenOut,
- slippagePercentage,
- gasPrice: dstGasPrice,
- recipient,
- source: RouterLiquiditySource.XSwap,
- })
- : undefined
-
- if (useRPOnDst && !isSuccessfulTradeReturn(_dstRPTrade!)) {
- throw new Error('getSquidCrossChainTrade: dstRPTrade failed')
- }
-
- const dstRPTrade = useRPOnDst
- ? (_dstRPTrade as SuccessfulTradeReturn)
- : undefined
-
- const routeRequest: RouteRequest = {
- fromAddress: from ?? zeroAddress,
- toAddress: recipient ?? zeroAddress,
- fromChain: srcChainId.toString(),
- toChain: dstChainId.toString(),
- fromToken: useRPOnSrc ? srcBridgeToken.address : tokenIn,
- toToken: useRPOnDst ? dstBridgeToken.address : tokenOut,
- fromAmount: dstAmountIn.toString(),
- slippage: +slippagePercentage,
- prefer: [DexName.SUSHISWAP_V3, DexName.SUSHISWAP_V2],
- quoteOnly: !from || !recipient,
- }
-
- if (useRPOnDst && dstRPTrade?.routeProcessorArgs) {
- const rpAddress = ROUTE_PROCESSOR_4_ADDRESS[dstChainId]
-
- // Transfer dstBridgeToken to RouteProcessor & call ProcessRoute()
- routeRequest.postHook = {
- chainType: ChainType.EVM,
- calls: [
- // Transfer full balance of dstBridgeToken to RouteProcessor
- {
- chainType: ChainType.EVM,
- callType: SquidCallType.FULL_TOKEN_BALANCE,
- target: dstBridgeToken.address,
- callData: encodeFunctionData({
- abi: erc20Abi,
- functionName: 'transfer',
- args: [rpAddress, 0n],
- }),
- value: '0',
- payload: {
- tokenAddress: dstBridgeToken.address,
- inputPos: 1,
- },
- estimatedGas: '30000',
- },
- // Invoke RouteProcessor.processRoute()
- {
- chainType: ChainType.EVM,
- callType: SquidCallType.DEFAULT,
- target: rpAddress,
- callData: encodeFunctionData({
- abi: routeProcessor4Abi_processRoute,
- functionName: 'processRoute',
- args: [
- dstRPTrade.routeProcessorArgs.tokenIn as Address,
- BigInt(dstRPTrade.routeProcessorArgs.amountIn),
- dstRPTrade.routeProcessorArgs.tokenOut as Address,
- BigInt(dstRPTrade.routeProcessorArgs.amountOutMin),
- dstRPTrade.routeProcessorArgs.to as Address,
- dstRPTrade.routeProcessorArgs.routeCode as Hex,
- ],
- }),
- value: '0',
- payload: {
- tokenAddress: zeroAddress,
- inputPos: 0,
- },
- estimatedGas: (1.2 * dstRPTrade!.gasSpent + 20_000).toString(),
- },
- ],
- description: `Swap ${tokenIn} -> ${tokenOut} on RouteProcessor`,
- } as Hook
- }
-
- const { route: squidRoute } = await getSquidRoute(routeRequest)
-
- const srcSquidTrade =
- isSrcSwap && !useRPOnSrc
- ? getSquidTrade(squidRoute.estimate.fromToken, srcBridgeToken)
- : undefined
-
- const dstSquidTrade =
- isDstSwap && !useRPOnDst
- ? getSquidTrade(dstBridgeToken, squidRoute.estimate.toToken)
- : undefined
-
- const dstAmountOut = useRPOnDst
- ? BigInt(dstRPTrade!.assumedAmountOut)
- : BigInt(squidRoute.estimate.toAmount)
-
- const dstAmountOutMin =
- useRPOnSrc && !isDstSwap
- ? applySlippage(srcRPTrade!.assumedAmountOut, slippagePercentage)
- : useRPOnDst
- ? applySlippage(dstRPTrade!.assumedAmountOut, slippagePercentage)
- : BigInt(squidRoute.estimate.toAmountMin)
-
- let priceImpact = 0
- if (useRPOnSrc) {
- priceImpact += srcRPTrade!.priceImpact
- }
- if (useRPOnDst) {
- priceImpact += dstRPTrade!.priceImpact
- }
-
- priceImpact += +squidRoute.estimate.aggregatePriceImpact / 100
-
- let writeArgs
- let functionName
- const transactionType =
- !isSrcSwap && !isDstSwap
- ? SushiXSwapTransactionType.Bridge
- : isSrcSwap && !isDstSwap
- ? SushiXSwapTransactionType.SwapAndBridge
- : !isSrcSwap && isDstSwap
- ? SushiXSwapTransactionType.BridgeAndSwap
- : SushiXSwapTransactionType.CrossChainSwap
-
- const srcTrade = useRPOnSrc ? srcRPTrade : srcSquidTrade
- const dstTrade = useRPOnDst ? dstRPTrade : dstSquidTrade
-
- if (!recipient || !from) {
- return {
- status: RouteStatus.Success,
- adapter: SushiXSwap2Adapter.Squid,
- priceImpact,
- amountIn: amount.toString(),
- amountOut: dstAmountOut.toString(),
- amountOutMin: dstAmountOutMin.toString(),
- tokenIn,
- tokenOut,
- srcBridgeToken: srcBridgeToken.serialize(),
- dstBridgeToken: dstBridgeToken.serialize(),
- srcTrade,
- dstTrade,
- transactionType,
- }
- }
-
- if (useRPOnSrc) {
- const srcSwapData = encodeRouteProcessorArgs(
- (srcTrade as SuccessfulTradeReturn).routeProcessorArgs!,
- )
-
- const squidCallData = decodeSquidRouterCallData(
- squidRoute.transactionRequest?.data as `0x${string}`,
- )
-
- const squidCallArgs =
- squidCallData.args && squidCallData.args.length > 1
- ? [squidCallData.args[0], 0, ...squidCallData.args.slice(2)]
- : undefined
-
- functionName = SushiXSwapFunctionName.SwapAndBridge
- writeArgs = [
- {
- refId: '0x0000',
- adapter: SQUID_ADAPTER_ADDRESS[srcChainId],
- tokenIn,
- amountIn: amount.toString(),
- to: recipient,
- adapterData: encodeSquidBridgeParams({
- srcBridgeToken,
- callData: encodeFunctionData({
- abi: squidRouterAbi,
- functionName: squidCallData.functionName,
- args: squidCallArgs,
- }),
- }),
- },
- recipient, // refundAddress
- srcSwapData, // srcSwapData
- '0x', // dstSwapData
- '0x', // dstPayloadData
- ]
- } else {
- functionName = SushiXSwapFunctionName.Bridge
- writeArgs = [
- {
- refId: '0x0000',
- adapter: SQUID_ADAPTER_ADDRESS[srcChainId],
- tokenIn,
- amountIn: amount.toString(),
- to: recipient,
- adapterData: encodeSquidBridgeParams({
- srcBridgeToken,
- callData: squidRoute.transactionRequest?.data as Hex,
- }),
- },
- recipient, // refundAddress
- '0x', // dstSwapData
- '0x', // dstPayloadData
- ]
- }
-
- // Add 10 % buffer
- const bridgeFee =
- (squidRoute.estimate.feeCosts.reduce(
- (accumulator, current) => accumulator + BigInt(current.amount),
- 0n,
- ) *
- 11n) /
- 10n
-
- const value =
- tokenIn.toLowerCase() === NativeAddress.toLowerCase()
- ? BigInt(amount) + BigInt(bridgeFee)
- : BigInt(bridgeFee)
-
- const srcGasEstimate =
- BigInt(squidRoute.transactionRequest?.gasLimit ?? 0) +
- (useRPOnSrc ? BigInt((srcTrade as SuccessfulTradeReturn).gasSpent) : 0n)
-
- const srcGasFee = srcGasPrice
- ? srcGasPrice * srcGasEstimate
- : srcGasEstimate
-
- const gasSpent = srcGasFee + bridgeFee
-
- return {
- adapter: SushiXSwap2Adapter.Squid,
- status: RouteStatus.Success,
- transactionType,
- tokenIn,
- tokenOut,
- srcBridgeToken: srcBridgeToken.serialize(),
- dstBridgeToken: dstBridgeToken.serialize(),
- amountIn: amount.toString(),
- amountOut: dstAmountOut.toString(),
- amountOutMin: dstAmountOutMin.toString(),
- srcTrade,
- dstTrade,
- priceImpact,
- gasSpent: gasSpent.toString(),
- bridgeFee: bridgeFee.toString(),
- srcGasFee: srcGasFee.toString(),
- writeArgs,
- functionName,
- value: value ? value.toString() : '0',
- }
- } catch (e) {
- console.error(e)
- return {
- adapter: SushiXSwap2Adapter.Squid,
- status: RouteStatus.NoWay,
- }
- }
-}
diff --git a/apps/web/src/lib/swap/cross-chain/actions/getSquidRoute.ts b/apps/web/src/lib/swap/cross-chain/actions/getSquidRoute.ts
deleted file mode 100644
index 5222015a46..0000000000
--- a/apps/web/src/lib/swap/cross-chain/actions/getSquidRoute.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { RouteRequest, RouteResponse } from '@0xsquid/squid-types'
-import { SquidApiURL, SquidIntegratorId } from 'sushi/config'
-
-export const getSquidRoute = async (
- params: RouteRequest,
-): Promise => {
- const url = new URL(`${SquidApiURL}/route`)
-
- const response = await fetch(url, {
- method: 'POST',
- body: JSON.stringify(params),
- headers: {
- 'x-integrator-id': SquidIntegratorId,
- 'Content-Type': 'application/json',
- },
- })
-
- const json = await response.json()
-
- if (response.status !== 200) {
- throw new Error(json.message)
- }
-
- return json
-}
diff --git a/apps/web/src/lib/swap/cross-chain/actions/getStargateCrossChainTrade.ts b/apps/web/src/lib/swap/cross-chain/actions/getStargateCrossChainTrade.ts
deleted file mode 100644
index 7e3fb7185f..0000000000
--- a/apps/web/src/lib/swap/cross-chain/actions/getStargateCrossChainTrade.ts
+++ /dev/null
@@ -1,405 +0,0 @@
-import { NativeAddress } from 'src/lib/constants'
-import { stargateAdapterAbi_getFee } from 'sushi/abi'
-import {
- STARGATE_ADAPTER_ADDRESS,
- STARGATE_CHAIN_ID,
- StargateAdapterChainId,
- isStargateAdapterChainId,
- publicClientConfig,
-} from 'sushi/config'
-import { RouteStatus, RouterLiquiditySource } from 'sushi/router'
-import {
- createPublicClient,
- encodeAbiParameters,
- parseAbiParameters,
-} from 'viem'
-import {
- STARGATE_SLIPPAGE_PERCENTAGE,
- SushiXSwap2Adapter,
- SushiXSwapFunctionName,
- SushiXSwapTransactionType,
- applySlippage,
- encodeRouteProcessorArgs,
- encodeStargateTeleportParams,
- estimateStargateDstGas,
- getStargateBridgePath,
-} from '../lib'
-import {
- CrossChainTradeSchemaType,
- GetCrossChainTradeParams,
-} from './getCrossChainTrade'
-import { getStargateFees } from './getStargateFees'
-import {
- SuccessfulTradeReturn,
- getTrade,
- isSuccessfulTradeReturn,
-} from './getTrade'
-
-export const getStargateCrossChainTrade = async ({
- srcChainId,
- dstChainId,
- tokenIn,
- tokenOut,
- amount,
- slippagePercentage,
- srcGasPrice,
- dstGasPrice,
- recipient,
-}: GetCrossChainTradeParams): Promise => {
- try {
- const bridgePath =
- isStargateAdapterChainId(srcChainId) &&
- isStargateAdapterChainId(dstChainId)
- ? getStargateBridgePath({ srcChainId, dstChainId, tokenIn, tokenOut })
- : undefined
-
- if (!bridgePath) {
- throw new Error('getStaragetCrossChainTrade: no bridge route found')
- }
-
- const { srcBridgeToken, dstBridgeToken } = bridgePath
-
- // has swap on source chain
- const isSrcSwap = Boolean(
- srcBridgeToken.isNative
- ? tokenIn.toLowerCase() !== NativeAddress.toLowerCase()
- : tokenIn.toLowerCase() !== srcBridgeToken.address.toLowerCase(),
- )
-
- // has swap on destination chain
- const isDstSwap = Boolean(
- dstBridgeToken.isNative
- ? tokenOut.toLowerCase() !== NativeAddress.toLowerCase()
- : tokenOut.toLowerCase() !== dstBridgeToken.address.toLowerCase(),
- )
-
- const _srcTrade = isSrcSwap
- ? await getTrade({
- chainId: srcChainId,
- amount,
- fromToken: tokenIn,
- toToken: srcBridgeToken.isNative
- ? NativeAddress
- : srcBridgeToken.address,
- slippagePercentage,
- gasPrice: srcGasPrice,
- recipient:
- STARGATE_ADAPTER_ADDRESS[srcChainId as StargateAdapterChainId],
- source: RouterLiquiditySource.XSwap,
- })
- : undefined
-
- if (isSrcSwap && !isSuccessfulTradeReturn(_srcTrade!)) {
- throw new Error('getStaragetCrossChainTrade: srcTrade failed')
- }
-
- const srcTrade = isSrcSwap
- ? (_srcTrade as SuccessfulTradeReturn)
- : undefined
-
- const bridgeFees = await getStargateFees({
- amount: isSrcSwap ? BigInt(srcTrade!.assumedAmountOut) : amount,
- srcBridgeToken,
- dstBridgeToken,
- })
-
- if (!bridgeFees) {
- throw new Error('getStaragetCrossChainTrade: getStargateFees failed')
- }
-
- const [eqFee, eqReward, lpFee, protocolFee] = bridgeFees
-
- const bridgeFeeAmount = eqFee - eqReward + lpFee + protocolFee
-
- const bridgeImpact =
- Number(bridgeFeeAmount) /
- Number(isSrcSwap ? srcTrade!.assumedAmountOut : amount)
-
- const srcAmountOut =
- (isSrcSwap ? BigInt(srcTrade!.assumedAmountOut) : amount) -
- bridgeFeeAmount
-
- const srcAmountOutMin = applySlippage(
- isSrcSwap
- ? applySlippage(srcTrade!.assumedAmountOut, slippagePercentage) -
- bridgeFeeAmount
- : srcAmountOut,
- STARGATE_SLIPPAGE_PERCENTAGE,
- )
-
- // adapted from amountSDtoLD in https://www.npmjs.com/package/@layerzerolabs/sg-sdk
- const dstAmountIn =
- (srcAmountOut * 10n ** BigInt(dstBridgeToken.decimals)) /
- 10n ** BigInt(srcBridgeToken.decimals)
- const dstAmountInMin =
- (srcAmountOutMin * 10n ** BigInt(dstBridgeToken.decimals)) /
- 10n ** BigInt(srcBridgeToken.decimals)
-
- const _dstTrade = isDstSwap
- ? await getTrade({
- chainId: dstChainId,
- amount: dstAmountIn,
- fromToken: dstBridgeToken.isNative
- ? NativeAddress
- : dstBridgeToken.address,
- toToken: tokenOut,
- slippagePercentage,
- gasPrice: dstGasPrice,
- recipient,
- source: RouterLiquiditySource.XSwap,
- })
- : undefined
-
- if (isDstSwap && !isSuccessfulTradeReturn(_dstTrade!)) {
- throw new Error('getStaragetCrossChainTrade: dstTrade failed')
- }
-
- const dstTrade = isDstSwap
- ? (_dstTrade as SuccessfulTradeReturn)
- : undefined
-
- const dstAmountOut = isDstSwap
- ? BigInt(dstTrade!.assumedAmountOut)
- : dstAmountIn
-
- const dstAmountOutMin = isDstSwap
- ? applySlippage(dstTrade!.assumedAmountOut, slippagePercentage)
- : dstAmountInMin
-
- let priceImpact = bridgeImpact
- if (isSrcSwap) priceImpact += srcTrade!.priceImpact
- if (isDstSwap) priceImpact += dstTrade!.priceImpact
-
- if (!recipient) {
- return {
- adapter: SushiXSwap2Adapter.Stargate,
- status: RouteStatus.Success,
- priceImpact,
- amountIn: amount.toString(),
- amountOut: dstAmountOut.toString(),
- amountOutMin: dstAmountOutMin.toString(),
- tokenIn,
- tokenOut,
- srcBridgeToken: srcBridgeToken.serialize(),
- dstBridgeToken: dstBridgeToken.serialize(),
- srcTrade,
- dstTrade,
- }
- }
-
- let writeArgs
- let functionName
- let dstPayload
- let dstGasEst = 0n
- let transactionType
-
- if (!isSrcSwap && !isDstSwap) {
- transactionType = SushiXSwapTransactionType.Bridge
- functionName = SushiXSwapFunctionName.Bridge
- writeArgs = [
- {
- refId: '0x0000',
- adapter:
- STARGATE_ADAPTER_ADDRESS[srcChainId as StargateAdapterChainId],
- tokenIn,
- amountIn: amount.toString(),
- to: recipient,
- adapterData: encodeStargateTeleportParams({
- srcBridgeToken,
- dstBridgeToken,
- amount: amount,
- amountMin: srcAmountOutMin,
- dustAmount: 0,
- receiver: recipient, // receivier is recipient because no dstPayload
- to: recipient,
- dstGas: dstGasEst,
- }),
- },
- recipient, // refundAddress
- '0x', // swapPayload
- '0x', // payloadData
- ]
- } else if (isSrcSwap && !isDstSwap) {
- const srcSwapData = encodeRouteProcessorArgs(
- srcTrade!.routeProcessorArgs!,
- )
-
- transactionType = SushiXSwapTransactionType.SwapAndBridge
- functionName = SushiXSwapFunctionName.SwapAndBridge
- writeArgs = [
- {
- refId: '0x0000',
- adapter:
- STARGATE_ADAPTER_ADDRESS[srcChainId as StargateAdapterChainId],
- tokenIn,
- amountIn: amount.toString(),
- to: recipient,
- adapterData: encodeStargateTeleportParams({
- srcBridgeToken,
- dstBridgeToken,
- amount: 0, // StargateAdapter sends srcBridgeToken to StargateComposer
- amountMin: srcAmountOutMin,
- dustAmount: 0,
- receiver: recipient, // receivier is recipient because no dstPayload
- to: recipient,
- dstGas: dstGasEst,
- }),
- },
- recipient, // refundAddress
- srcSwapData,
- '0x',
- '0x',
- ]
- } else if (!isSrcSwap) {
- const dstSwapData = encodeRouteProcessorArgs(
- dstTrade!.routeProcessorArgs!,
- )
-
- dstGasEst = estimateStargateDstGas(dstTrade!.gasSpent)
-
- dstPayload = encodeAbiParameters(
- parseAbiParameters('address, bytes, bytes'),
- [
- recipient,
- dstSwapData,
- '0x', // payloadData
- ],
- )
-
- transactionType = SushiXSwapTransactionType.BridgeAndSwap
- functionName = SushiXSwapFunctionName.Bridge
- writeArgs = [
- {
- refId: '0x0000',
- adapter:
- STARGATE_ADAPTER_ADDRESS[srcChainId as StargateAdapterChainId],
- tokenIn,
- amountIn: amount.toString(),
- to: recipient,
- adapterData: encodeStargateTeleportParams({
- srcBridgeToken,
- dstBridgeToken,
- amount: amount,
- amountMin: srcAmountOutMin,
- dustAmount: 0,
- receiver:
- STARGATE_ADAPTER_ADDRESS[dstChainId as StargateAdapterChainId],
- to: recipient,
- dstGas: dstGasEst,
- }),
- },
- recipient, // refundAddress
- dstSwapData,
- '0x', // dstPayload
- ]
- } else if (isSrcSwap && isDstSwap) {
- const srcSwapData = encodeRouteProcessorArgs(
- srcTrade!.routeProcessorArgs!,
- )
- const dstSwapData = encodeRouteProcessorArgs(
- dstTrade!.routeProcessorArgs!,
- )
-
- dstPayload = encodeAbiParameters(
- parseAbiParameters('address, bytes, bytes'),
- [
- recipient, // to
- dstSwapData, // swapData
- '0x', // payloadData
- ],
- )
- dstGasEst = estimateStargateDstGas(dstTrade!.gasSpent)
-
- transactionType = SushiXSwapTransactionType.CrossChainSwap
- functionName = SushiXSwapFunctionName.SwapAndBridge
- writeArgs = [
- {
- refId: '0x0000',
- adapter:
- STARGATE_ADAPTER_ADDRESS[srcChainId as StargateAdapterChainId],
- tokenIn,
- amountIn: amount.toString(),
- to: recipient,
- adapterData: encodeStargateTeleportParams({
- srcBridgeToken,
- dstBridgeToken,
- amount: 0, // StargateAdapter sends srcBridgeToken to StargateComposer
- amountMin: srcAmountOutMin,
- dustAmount: 0,
- receiver:
- STARGATE_ADAPTER_ADDRESS[dstChainId as StargateAdapterChainId],
- to: recipient,
- dstGas: dstGasEst,
- }),
- },
- recipient, // refundAddress
- srcSwapData, //srcSwapPayload
- dstSwapData, // dstPayload
- '0x',
- ]
- } else {
- throw new Error('Crosschain swap not found.')
- }
-
- const client = createPublicClient(publicClientConfig[srcChainId])
-
- let [lzFee] = await client.readContract({
- address: STARGATE_ADAPTER_ADDRESS[srcChainId as StargateAdapterChainId],
- abi: stargateAdapterAbi_getFee,
- functionName: 'getFee',
- args: [
- STARGATE_CHAIN_ID[dstChainId as StargateAdapterChainId], // dstChain
- 1, // functionType
- isDstSwap
- ? STARGATE_ADAPTER_ADDRESS[dstChainId as StargateAdapterChainId]
- : recipient, // receiver
- dstGasEst, // gasAmount
- 0n, // dustAmount
- isDstSwap ? dstPayload! : '0x', // payload
- ],
- })
-
- // Add 20% buffer to LZ fee
- lzFee = (lzFee * 5n) / 4n
-
- const value =
- tokenIn.toLowerCase() === NativeAddress.toLowerCase()
- ? BigInt(amount) + lzFee
- : lzFee
-
- // est 500K gas for XSwapV2 call
- const srcGasEst = 500000n + BigInt(srcTrade?.gasSpent ?? 0)
-
- const srcGasFee = srcGasPrice ? srcGasPrice * srcGasEst : srcGasEst
-
- const gasSpent = srcGasFee + lzFee
-
- return {
- adapter: SushiXSwap2Adapter.Stargate,
- status: RouteStatus.Success,
- transactionType,
- tokenIn,
- tokenOut,
- srcBridgeToken: srcBridgeToken.serialize(),
- dstBridgeToken: dstBridgeToken.serialize(),
- amountIn: amount.toString(),
- amountOut: dstAmountOut.toString(),
- amountOutMin: dstAmountOutMin.toString(),
- srcTrade,
- dstTrade,
- priceImpact,
- gasSpent: gasSpent.toString(),
- bridgeFee: lzFee.toString(),
- srcGasFee: srcGasFee.toString(),
- writeArgs,
- functionName,
- value: value ? value.toString() : '0',
- }
- } catch (e) {
- console.error(e)
- return {
- adapter: SushiXSwap2Adapter.Stargate,
- status: RouteStatus.NoWay,
- }
- }
-}
diff --git a/apps/web/src/lib/swap/cross-chain/actions/getStargateFees.ts b/apps/web/src/lib/swap/cross-chain/actions/getStargateFees.ts
deleted file mode 100644
index 1dcba0edaf..0000000000
--- a/apps/web/src/lib/swap/cross-chain/actions/getStargateFees.ts
+++ /dev/null
@@ -1,180 +0,0 @@
-import {
- stargateFeeLibraryV03Abi_getFees,
- stargatePoolAbi_feeLibrary,
- stargatePoolAbi_getChainPath,
- stargatePoolAbi_sharedDecimals,
-} from 'sushi/abi'
-import {
- STARGATE_ADAPTER_ADDRESS,
- STARGATE_CHAIN_ID,
- STARGATE_ETH_ADDRESS,
- STARGATE_POOL_ADDRESS,
- STARGATE_POOL_ID,
- StargateAdapterChainId,
- StargateChainId,
- publicClientConfig,
-} from 'sushi/config'
-import { Amount, Type } from 'sushi/currency'
-import { Address, createPublicClient, zeroAddress } from 'viem'
-
-interface GetStargateFeesParams {
- amount: bigint
- srcBridgeToken: Type
- dstBridgeToken: Type
-}
-
-export const getStargateFees = async ({
- amount: _amount,
- srcBridgeToken,
- dstBridgeToken,
-}: GetStargateFeesParams) => {
- if (!_amount) return undefined
-
- const client = createPublicClient(publicClientConfig[srcBridgeToken.chainId])
-
- const stargatePoolResults = await getStargatePool({
- srcBridgeToken,
- dstBridgeToken,
- })
-
- const amount = Amount.fromRawAmount(srcBridgeToken, _amount)
-
- const adjusted = (() => {
- if (!stargatePoolResults?.[2]?.result) return undefined
- const localDecimals = BigInt(amount.currency.decimals)
- const sharedDecimals = stargatePoolResults[2].result
- if (localDecimals === sharedDecimals) return amount
- return localDecimals > sharedDecimals
- ? amount.asFraction.divide(10n ** (localDecimals - sharedDecimals))
- : amount.asFraction.multiply(10n ** (sharedDecimals - localDecimals))
- })()
-
- const feesResults = await client.readContract({
- address: stargatePoolResults?.[1]?.result ?? zeroAddress,
- functionName: 'getFees',
- args: [
- BigInt(
- STARGATE_POOL_ID[srcBridgeToken.chainId][
- srcBridgeToken.isNative
- ? STARGATE_ETH_ADDRESS[
- srcBridgeToken.chainId as keyof typeof STARGATE_ETH_ADDRESS
- ]
- : srcBridgeToken.address
- ] ?? 0,
- ),
- BigInt(
- STARGATE_POOL_ID[dstBridgeToken.chainId][
- dstBridgeToken.isNative
- ? STARGATE_ETH_ADDRESS[
- dstBridgeToken.chainId as keyof typeof STARGATE_ETH_ADDRESS
- ]
- : dstBridgeToken.address
- ] ?? 0,
- ),
- STARGATE_CHAIN_ID[dstBridgeToken.chainId as StargateChainId],
- STARGATE_ADAPTER_ADDRESS[
- srcBridgeToken.chainId as StargateAdapterChainId
- ] as Address,
- BigInt(adjusted?.quotient ?? 0),
- ],
- abi: stargateFeeLibraryV03Abi_getFees,
- })
-
- if (
- !amount ||
- !feesResults ||
- !stargatePoolResults?.[1]?.result ||
- !stargatePoolResults?.[2]?.result ||
- !srcBridgeToken ||
- !dstBridgeToken
- ) {
- return undefined
- }
-
- const localDecimals = BigInt(amount.currency.decimals)
- const sharedDecimals = stargatePoolResults[2].result
-
- const { eqFee, eqReward, lpFee, protocolFee } = feesResults
-
- if (localDecimals === sharedDecimals)
- return [eqFee, eqReward, lpFee, protocolFee]
-
- const _eqFee =
- localDecimals > sharedDecimals
- ? eqFee * 10n ** (localDecimals - sharedDecimals)
- : eqFee / 10n ** (sharedDecimals - localDecimals)
-
- const _eqReward =
- localDecimals > sharedDecimals
- ? eqReward * 10n ** (localDecimals - sharedDecimals)
- : eqReward / 10n ** (sharedDecimals - localDecimals)
-
- const _lpFee =
- localDecimals > sharedDecimals
- ? lpFee * 10n ** (localDecimals - sharedDecimals)
- : lpFee / 10n ** (sharedDecimals - localDecimals)
-
- const _protocolFee =
- localDecimals > sharedDecimals
- ? protocolFee * 10n ** (localDecimals - sharedDecimals)
- : protocolFee / 10n ** (sharedDecimals - localDecimals)
-
- return [_eqFee, _eqReward, _lpFee, _protocolFee]
-}
-
-const getStargatePool = async ({
- srcBridgeToken,
- dstBridgeToken,
-}: Omit) => {
- const client = createPublicClient(publicClientConfig[srcBridgeToken.chainId])
-
- return client.multicall({
- contracts: [
- {
- address: STARGATE_POOL_ADDRESS[srcBridgeToken.chainId][
- srcBridgeToken.isNative
- ? STARGATE_ETH_ADDRESS[
- srcBridgeToken.chainId as keyof typeof STARGATE_ETH_ADDRESS
- ]
- : srcBridgeToken.address
- ] as Address,
- functionName: 'getChainPath',
- args: [
- STARGATE_CHAIN_ID[dstBridgeToken.chainId as StargateChainId],
- BigInt(
- STARGATE_POOL_ID[dstBridgeToken.chainId][
- dstBridgeToken.isNative
- ? STARGATE_ETH_ADDRESS[
- dstBridgeToken.chainId as keyof typeof STARGATE_ETH_ADDRESS
- ]
- : dstBridgeToken.address
- ] ?? 0,
- ),
- ],
- abi: stargatePoolAbi_getChainPath,
- },
- {
- address: STARGATE_POOL_ADDRESS[srcBridgeToken.chainId][
- srcBridgeToken.isNative
- ? STARGATE_ETH_ADDRESS[
- srcBridgeToken.chainId as keyof typeof STARGATE_ETH_ADDRESS
- ]
- : srcBridgeToken.address
- ] as Address,
- functionName: 'feeLibrary',
- abi: stargatePoolAbi_feeLibrary,
- },
- {
- address: STARGATE_POOL_ADDRESS[srcBridgeToken.chainId][
- srcBridgeToken.isNative
- ? STARGATE_ETH_ADDRESS[
- srcBridgeToken.chainId as keyof typeof STARGATE_ETH_ADDRESS
- ]
- : srcBridgeToken.address
- ] as Address,
- functionName: 'sharedDecimals',
- abi: stargatePoolAbi_sharedDecimals,
- },
- ] as const,
- })
-}
diff --git a/apps/web/src/lib/swap/cross-chain/actions/getTrade.ts b/apps/web/src/lib/swap/cross-chain/actions/getTrade.ts
deleted file mode 100644
index d159a6516a..0000000000
--- a/apps/web/src/lib/swap/cross-chain/actions/getTrade.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import {
- UseTradeParams,
- // getTradeQueryApiVersion,
- tradeValidator02,
-} from 'src/lib/hooks/react-query'
-import { API_BASE_URL } from 'sushi/config'
-import { RouteStatus } from 'sushi/router'
-import { Address } from 'viem'
-import { z } from 'zod'
-
-export type GetTrade = Pick<
- UseTradeParams,
- 'chainId' | 'gasPrice' | 'slippagePercentage' | 'recipient' | 'source'
-> & {
- fromToken: Address
- toToken: Address
- amount: bigint
-}
-
-export type GetTradeReturn = z.infer
-
-export type SuccessfulTradeReturn = Extract<
- GetTradeReturn,
- { status: 'Success' | 'Partial' }
->
-
-export const isSuccessfulTradeReturn = (
- trade: GetTradeReturn,
-): trade is SuccessfulTradeReturn => trade.status === RouteStatus.Success
-
-export const getTrade = async ({
- chainId,
- fromToken,
- toToken,
- amount,
- gasPrice = 50n,
- slippagePercentage,
- recipient,
- source,
-}: GetTrade) => {
- const params = new URL(`${API_BASE_URL}/swap/v4/${chainId}`)
- params.searchParams.set('chainId', `${chainId}`)
- params.searchParams.set('tokenIn', `${fromToken}`)
- params.searchParams.set('tokenOut', `${toToken}`)
- params.searchParams.set('amount', `${amount.toString()}`)
- params.searchParams.set('maxPriceImpact', `${+slippagePercentage / 100}`)
- params.searchParams.set('gasPrice', `${gasPrice}`)
- recipient && params.searchParams.set('to', `${recipient}`)
- params.searchParams.set('preferSushi', 'true')
- if (source !== undefined) params.searchParams.set('source', `${source}`)
-
- const res = await fetch(params.toString())
- const json = await res.json()
- const resp = tradeValidator02.parse(json)
- return resp
-}
diff --git a/apps/web/src/lib/swap/cross-chain/actions/index.ts b/apps/web/src/lib/swap/cross-chain/actions/index.ts
deleted file mode 100644
index 27ea0665af..0000000000
--- a/apps/web/src/lib/swap/cross-chain/actions/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export * from './getCrossChainTrade'
-export * from './getCrossChainTrades'
diff --git a/apps/web/src/lib/swap/cross-chain/hooks/index.ts b/apps/web/src/lib/swap/cross-chain/hooks/index.ts
index 38382c1eec..5bbdfaf967 100644
--- a/apps/web/src/lib/swap/cross-chain/hooks/index.ts
+++ b/apps/web/src/lib/swap/cross-chain/hooks/index.ts
@@ -1,2 +1 @@
-export * from './useAxelarScanLink'
-export * from './useLayerZeroScanLink'
+export * from './useLifiScanLink'
diff --git a/apps/web/src/lib/swap/cross-chain/hooks/useAxelarScanLink.ts b/apps/web/src/lib/swap/cross-chain/hooks/useAxelarScanLink.ts
deleted file mode 100644
index 9cb0bd8ed7..0000000000
--- a/apps/web/src/lib/swap/cross-chain/hooks/useAxelarScanLink.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { StatusResponse } from '@0xsquid/squid-types'
-import { useQuery } from '@tanstack/react-query'
-import { ChainId } from 'sushi/chain'
-import { SquidApiURL, SquidIntegratorId } from 'sushi/config'
-
-export const getSquidStatus = async (
- txHash: string,
-): Promise => {
- const url = new URL(`${SquidApiURL}/status`)
- url.searchParams.set('transactionId', txHash)
-
- const response = await fetch(url, {
- headers: {
- 'x-integrator-id': SquidIntegratorId,
- },
- })
-
- const json = await response.json()
-
- return json
-}
-
-export const useAxelarScanLink = ({
- tradeId,
- network0,
- network1,
- txHash,
- enabled,
-}: {
- tradeId: string
- network0: ChainId
- network1: ChainId
- txHash: string | undefined
- enabled?: boolean
-}) => {
- return useQuery({
- queryKey: ['axelarScanLink', { txHash, network0, network1, tradeId }],
- queryFn: async () => {
- if (txHash) {
- return getSquidStatus(txHash).then((data) => ({
- link: data.axelarTransactionUrl,
- status: data.squidTransactionStatus,
- dstTxHash: data.toChain?.transactionId,
- }))
- }
-
- return {
- link: undefined,
- status: undefined,
- dstTxHash: undefined,
- }
- },
- refetchInterval: 2000,
- enabled: enabled && !!txHash,
- })
-}
diff --git a/apps/web/src/lib/swap/cross-chain/hooks/useLayerZeroScanLink.ts b/apps/web/src/lib/swap/cross-chain/hooks/useLayerZeroScanLink.ts
deleted file mode 100644
index c19dd5f18b..0000000000
--- a/apps/web/src/lib/swap/cross-chain/hooks/useLayerZeroScanLink.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { createClient } from '@layerzerolabs/scan-client'
-import { useQuery } from '@tanstack/react-query'
-import { ChainId } from 'sushi/chain'
-import { STARGATE_CHAIN_ID } from 'sushi/config'
-
-const client = createClient('mainnet')
-
-export const useLayerZeroScanLink = ({
- tradeId,
- network0,
- network1,
- txHash,
- enabled,
-}: {
- tradeId: string
- network0: ChainId
- network1: ChainId
- txHash: string | undefined
- enabled?: boolean
-}) => {
- return useQuery({
- queryKey: ['lzLink', { txHash, network0, network1, tradeId }],
- queryFn: async () => {
- if (
- txHash &&
- network0 in STARGATE_CHAIN_ID &&
- network1 in STARGATE_CHAIN_ID
- ) {
- const result = await client.getMessagesBySrcTxHash(txHash)
- if (result.messages.length > 0) {
- const { status, dstTxHash } = result.messages[0]
-
- return {
- link: `https://layerzeroscan.com/tx/${txHash}`,
- status,
- dstTxHash,
- }
- }
- }
-
- return {
- link: undefined,
- status: undefined,
- dstTxHash: undefined,
- }
- },
- refetchInterval: 2000,
- enabled: enabled && !!txHash,
- })
-}
diff --git a/apps/web/src/lib/swap/cross-chain/hooks/useLifiScanLink.ts b/apps/web/src/lib/swap/cross-chain/hooks/useLifiScanLink.ts
new file mode 100644
index 0000000000..ae38416e80
--- /dev/null
+++ b/apps/web/src/lib/swap/cross-chain/hooks/useLifiScanLink.ts
@@ -0,0 +1,59 @@
+import { useQuery } from '@tanstack/react-query'
+import { Hex } from 'viem'
+import { z } from 'zod'
+
+const LiFiStatusResponseSchema = z.object({
+ sending: z.object({
+ txHash: z.string().transform((txHash) => txHash as Hex),
+ }),
+ receiving: z
+ .object({
+ txHash: z.string().transform((txHash) => txHash as Hex),
+ })
+ .optional(),
+ lifiExplorerLink: z.string().optional(),
+ status: z.string().optional(),
+ substatus: z.string().optional(),
+})
+
+type LiFiStatusResponseType = z.infer
+
+const getLiFiStatus = async (
+ txHash: string,
+): Promise => {
+ const url = new URL('https://li.quest/v1/status')
+ url.searchParams.set('txHash', txHash)
+
+ const response = await fetch(url)
+
+ const json = await response.json()
+
+ return json
+}
+
+interface UseLiFiStatusParams {
+ txHash: Hex | undefined
+ tradeId: string
+ enabled?: boolean
+}
+
+export const useLiFiStatus = ({
+ txHash,
+ tradeId,
+ enabled = true,
+}: UseLiFiStatusParams) => {
+ return useQuery({
+ queryKey: ['lifiStatus', { tradeId }],
+ queryFn: async () => {
+ if (!txHash) throw new Error('txHash is required')
+
+ return getLiFiStatus(txHash)
+ },
+ refetchInterval: 5000,
+ enabled: ({ state: { data } }) =>
+ enabled &&
+ !!txHash &&
+ data?.status !== 'DONE' &&
+ data?.status !== 'FAILED',
+ })
+}
diff --git a/apps/web/src/lib/swap/cross-chain/index.ts b/apps/web/src/lib/swap/cross-chain/index.ts
index 2b743fece2..6ab3bc1bf8 100644
--- a/apps/web/src/lib/swap/cross-chain/index.ts
+++ b/apps/web/src/lib/swap/cross-chain/index.ts
@@ -1,3 +1,4 @@
-export * from './actions'
export * from './hooks'
-export * from './lib'
+export * from './utils'
+export * from './schema'
+export * from './types'
diff --git a/apps/web/src/lib/swap/cross-chain/lib/SquidAdapter.ts b/apps/web/src/lib/swap/cross-chain/lib/SquidAdapter.ts
deleted file mode 100644
index 631ad020f8..0000000000
--- a/apps/web/src/lib/swap/cross-chain/lib/SquidAdapter.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { Token as SquidToken } from '@0xsquid/squid-types'
-import { tradeValidator02 } from 'src/lib/hooks/react-query'
-import { squidRouterAbi } from 'sushi/abi'
-import { ChainId } from 'sushi/chain'
-import { SquidAdapterChainId } from 'sushi/config'
-import { Token } from 'sushi/currency'
-import { RouteStatus } from 'sushi/router'
-import {
- Hex,
- decodeFunctionData,
- encodeAbiParameters,
- parseAbiParameters,
-} from 'viem'
-import { z } from 'zod'
-
-export const isSquidRouteProcessorEnabled: Record<
- SquidAdapterChainId,
- boolean
-> = {
- [ChainId.ETHEREUM]: true,
- [ChainId.BSC]: true,
- [ChainId.AVALANCHE]: true,
- [ChainId.POLYGON]: true,
- [ChainId.ARBITRUM]: true,
- [ChainId.OPTIMISM]: true,
- [ChainId.BASE]: true,
- [ChainId.FANTOM]: true,
- [ChainId.LINEA]: true,
- [ChainId.KAVA]: true,
- [ChainId.MOONBEAM]: false,
- [ChainId.CELO]: true,
- [ChainId.SCROLL]: true,
- [ChainId.FILECOIN]: true,
- [ChainId.BLAST]: true,
-}
-
-/*
- SquidBridgeParams {
- address token; // token being bridged
- bytes squidRouterData; // abi-encoded squidRouter calldata
- }
-*/
-export const encodeSquidBridgeParams = ({
- srcBridgeToken,
- callData,
-}: {
- srcBridgeToken: Token
- callData: Hex
-}) => {
- return encodeAbiParameters(parseAbiParameters('address, bytes'), [
- srcBridgeToken.address,
- callData,
- ])
-}
-
-export const decodeSquidRouterCallData = (data: `0x${string}`) => {
- return decodeFunctionData({ abi: squidRouterAbi, data })
-}
-
-// this is only used for route preview
-export const getSquidTrade = (
- fromToken: SquidToken | Token,
- toToken: SquidToken | Token,
-): z.infer => {
- return {
- status: RouteStatus.Success,
- tokens: [
- {
- name: fromToken.name ?? '',
- symbol: fromToken.symbol ?? '',
- decimals: fromToken.decimals,
- address: fromToken.address,
- },
- {
- name: toToken.name ?? '',
- symbol: toToken.symbol ?? '',
- decimals: toToken.decimals,
- address: toToken.address,
- },
- ],
- tokenFrom: 0,
- tokenTo: 1,
- primaryPrice: 0,
- swapPrice: 0,
- priceImpact: 0,
- amountIn: '',
- assumedAmountOut: '',
- gasSpent: 0,
- }
-}
diff --git a/apps/web/src/lib/swap/cross-chain/lib/StargateAdapter.ts b/apps/web/src/lib/swap/cross-chain/lib/StargateAdapter.ts
deleted file mode 100644
index 76fe5e2f0d..0000000000
--- a/apps/web/src/lib/swap/cross-chain/lib/StargateAdapter.ts
+++ /dev/null
@@ -1,152 +0,0 @@
-import { NativeAddress } from 'src/lib/constants'
-import {
- STARGATE_CHAIN_ID,
- STARGATE_CHAIN_PATHS,
- STARGATE_ETH_ADDRESS,
- STARGATE_POOL_ID,
- STARGATE_USDC,
- STARGATE_USDC_ADDRESS,
- STARGATE_USDT,
- STARGATE_USDT_ADDRESS,
- StargateAdapterChainId,
-} from 'sushi/config'
-import { Native, Type } from 'sushi/currency'
-import { Address, encodeAbiParameters, parseAbiParameters } from 'viem'
-
-export const STARGATE_SLIPPAGE_PERCENTAGE = 1 // 1%
-
-/*
- struct StargateTeleportParams {
- uint16 dstChainId; // stargate dst chain id
- address token; // token getting bridged
- uint256 srcPoolId; // stargate src pool id
- uint256 dstPoolId; // stargate dst pool id
- uint256 amount; // amount to bridge
- uint256 amountMin; // amount to bridge minimum
- uint256 dustAmount; // native token to be received on dst chain
- address receiver; // detination address for sgReceive
- address to; // address for fallback tranfers on sgReceive
- uint256 gas; // extra gas to be sent for dst chain operations
- }
-*/
-
-export const encodeStargateTeleportParams = ({
- srcBridgeToken,
- dstBridgeToken,
- amount,
- amountMin,
- dustAmount,
- receiver,
- to,
- dstGas,
-}: {
- srcBridgeToken: Type
- dstBridgeToken: Type
- amount: Parameters[0]
- amountMin: Parameters[0]
- dustAmount: Parameters[0]
- receiver: Address
- to: Address
- dstGas: Parameters[0]
-}): string => {
- return encodeAbiParameters(
- parseAbiParameters(
- 'uint16, address, uint256, uint256, uint256, uint256, uint256, address, address, uint256',
- ),
- [
- STARGATE_CHAIN_ID[dstBridgeToken.chainId as StargateAdapterChainId],
- srcBridgeToken.isNative
- ? '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
- : (srcBridgeToken.address as Address),
- BigInt(
- STARGATE_POOL_ID[srcBridgeToken.chainId as StargateAdapterChainId][
- srcBridgeToken.isNative
- ? STARGATE_ETH_ADDRESS[
- srcBridgeToken.chainId as keyof typeof STARGATE_ETH_ADDRESS
- ]
- : srcBridgeToken.address
- ],
- ),
- BigInt(
- STARGATE_POOL_ID[dstBridgeToken.chainId as StargateAdapterChainId][
- dstBridgeToken.isNative
- ? STARGATE_ETH_ADDRESS[
- dstBridgeToken.chainId as keyof typeof STARGATE_ETH_ADDRESS
- ]
- : dstBridgeToken.address
- ],
- ),
- BigInt(amount),
- BigInt(amountMin),
- BigInt(dustAmount),
- receiver,
- to,
- BigInt(dstGas),
- ],
- )
-}
-
-// estiamte gas in sgReceive()
-export const estimateStargateDstGas = (gasUsed: number) => {
- // estGas = (150K + gasSpentTines * 1.25)
- return BigInt(Math.floor(gasUsed * 1.25) + 150000)
-}
-
-export const getStargateBridgePath = ({
- srcChainId,
- dstChainId,
- tokenIn,
-}: {
- srcChainId: StargateAdapterChainId
- dstChainId: StargateAdapterChainId
- tokenIn: Address
- tokenOut: Address
-}) => {
- const srcChainPaths = STARGATE_CHAIN_PATHS[srcChainId]
-
- // If srcCurrency is ETH, check for ETH path
- if (
- tokenIn.toLowerCase() === NativeAddress.toLowerCase() &&
- srcChainId in STARGATE_ETH_ADDRESS
- ) {
- const ethPaths =
- srcChainPaths[
- STARGATE_ETH_ADDRESS[srcChainId as keyof typeof STARGATE_ETH_ADDRESS]
- ]
-
- if (
- ethPaths.find((dstBridgeToken) => dstBridgeToken.chainId === dstChainId)
- ) {
- return {
- srcBridgeToken: Native.onChain(srcChainId),
- dstBridgeToken: Native.onChain(dstChainId),
- }
- }
- }
-
- // Else fallback to USDC/USDT
- if (
- srcChainId in STARGATE_USDC_ADDRESS ||
- srcChainId in STARGATE_USDT_ADDRESS
- ) {
- const srcBridgeToken =
- srcChainId in STARGATE_USDC
- ? STARGATE_USDC[srcChainId as keyof typeof STARGATE_USDC]
- : STARGATE_USDT[srcChainId as keyof typeof STARGATE_USDT]
-
- const usdPaths = srcChainPaths[srcBridgeToken.address as Address]
-
- const dstBridgeToken = usdPaths.find(
- (dstBridgeToken) => dstBridgeToken.chainId === dstChainId,
- )
-
- if (dstBridgeToken) {
- return {
- srcBridgeToken,
- dstBridgeToken,
- }
- }
- }
-
- return undefined
-}
diff --git a/apps/web/src/lib/swap/cross-chain/lib/SushiXSwap.ts b/apps/web/src/lib/swap/cross-chain/lib/SushiXSwap.ts
deleted file mode 100644
index 9489780475..0000000000
--- a/apps/web/src/lib/swap/cross-chain/lib/SushiXSwap.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { sushiXSwap2Abi_bridge, sushiXSwap2Abi_swapAndBridge } from 'sushi/abi'
-import {
- Address,
- Hex,
- WriteContractParameters,
- encodeAbiParameters,
- parseAbiParameters,
-} from 'viem'
-import { SuccessfulTradeReturn } from '../actions/getTrade'
-
-export enum SushiXSwap2Adapter {
- Stargate = 'Stargate',
- Squid = 'Squid',
-}
-
-export enum SushiXSwapTransactionType {
- Bridge = 'Bridge',
- SwapAndBridge = 'SwapAndBridge',
- BridgeAndSwap = 'BridgeAndSwap',
- CrossChainSwap = 'CrossChainSwap',
-}
-
-export enum SushiXSwapFunctionName {
- Bridge = 'bridge',
- SwapAndBridge = 'swapAndBridge',
-}
-
-export type SushiXSwapWriteArgsBridge = WriteContractParameters<
- typeof sushiXSwap2Abi_bridge,
- SushiXSwapFunctionName.Bridge
->['args']
-
-export type SushiXSwapWriteArgsSwapAndBridge = WriteContractParameters<
- typeof sushiXSwap2Abi_swapAndBridge,
- SushiXSwapFunctionName.SwapAndBridge
->['args']
-
-export type SushiXSwapWriteArgs =
- | SushiXSwapWriteArgsBridge
- | SushiXSwapWriteArgsSwapAndBridge
-
-export const encodePayloadData = ({
- target,
- gasLimit,
- targetData,
-}: {
- target: Address
- gasLimit: bigint
- targetData: `0x${string}`
-}) => {
- return encodeAbiParameters(parseAbiParameters('address, uint256, bytes'), [
- target,
- gasLimit,
- targetData,
- ])
-}
-
-type ProcessRouteInput = readonly [
- Address,
- bigint,
- Address,
- bigint,
- Address,
- `0x${string}`,
-]
-
-export function encodeSwapData([
- tokenIn,
- amountIn,
- tokenOut,
- amountOut,
- to,
- route,
-]: ProcessRouteInput) {
- return encodeAbiParameters(
- parseAbiParameters(
- '(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOut, address to, bytes route)',
- ),
- [{ tokenIn, amountIn, tokenOut, amountOut, to, route }],
- )
-}
-
-export function encodeRouteProcessorArgs({
- tokenIn,
- amountIn,
- tokenOut,
- amountOutMin,
- to,
- routeCode,
-}: NonNullable) {
- return encodeAbiParameters(
- parseAbiParameters(
- '(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOut, address to, bytes route)',
- ),
- [
- {
- tokenIn: tokenIn as Address,
- amountIn: BigInt(amountIn),
- tokenOut: tokenOut as Address,
- amountOut: BigInt(amountOutMin),
- to: to as Address,
- route: routeCode as Hex,
- },
- ],
- )
-}
diff --git a/apps/web/src/lib/swap/cross-chain/lib/index.ts b/apps/web/src/lib/swap/cross-chain/lib/index.ts
deleted file mode 100644
index ac0768b5cd..0000000000
--- a/apps/web/src/lib/swap/cross-chain/lib/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './SquidAdapter'
-export * from './StargateAdapter'
-export * from './SushiXSwap'
-export * from './utils'
diff --git a/apps/web/src/lib/swap/cross-chain/lib/utils.ts b/apps/web/src/lib/swap/cross-chain/lib/utils.ts
deleted file mode 100644
index 70ea05346b..0000000000
--- a/apps/web/src/lib/swap/cross-chain/lib/utils.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { BigintIsh, getBigInt } from 'sushi/math'
-
-export const applySlippage = (
- amount: BigintIsh,
- slippagePercentage: string | number,
-) => {
- return (
- (BigInt(amount) * getBigInt((1 - +slippagePercentage / 100) * 1_000_000)) /
- 1_000_000n
- )
-}
diff --git a/apps/web/src/lib/swap/cross-chain/schema.ts b/apps/web/src/lib/swap/cross-chain/schema.ts
new file mode 100644
index 0000000000..f684d5fadb
--- /dev/null
+++ b/apps/web/src/lib/swap/cross-chain/schema.ts
@@ -0,0 +1,154 @@
+import { isXSwapSupportedChainId } from 'src/config'
+import { hexToBigInt, isAddress, isHex } from 'viem'
+import { z } from 'zod'
+
+export const crossChainTokenSchema = z.object({
+ address: z.string().refine((address) => isAddress(address), {
+ message: 'address does not conform to Address',
+ }),
+ decimals: z.number(),
+ symbol: z.string(),
+ chainId: z.number().refine((chainId) => isXSwapSupportedChainId(chainId), {
+ message: `chainId must exist in XSwapChainId`,
+ }),
+ name: z.string(),
+ priceUSD: z.string(),
+})
+
+export const crossChainActionSchema = z.object({
+ fromChainId: z
+ .number()
+ .refine((chainId) => isXSwapSupportedChainId(chainId), {
+ message: `fromChainId must exist in XSwapChainId`,
+ }),
+ fromAmount: z.string().transform((amount) => BigInt(amount)),
+ fromToken: crossChainTokenSchema,
+ toChainId: z.number().refine((chainId) => isXSwapSupportedChainId(chainId), {
+ message: `toChainId must exist in XSwapChainId`,
+ }),
+ toToken: crossChainTokenSchema,
+ slippage: z.number(),
+ fromAddress: z
+ .string()
+ .refine((address) => isAddress(address), {
+ message: 'fromAddress does not conform to Address',
+ })
+ .optional(),
+ toAddress: z
+ .string()
+ .refine((address) => isAddress(address), {
+ message: 'toAddress does not conform to Address',
+ })
+ .optional(),
+})
+
+export const crossChainEstimateSchema = z.object({
+ tool: z.string(),
+ fromAmount: z.string().transform((amount) => BigInt(amount)),
+ toAmount: z.string().transform((amount) => BigInt(amount)),
+ toAmountMin: z.string().transform((amount) => BigInt(amount)),
+ approvalAddress: z.string().refine((address) => isAddress(address), {
+ message: 'approvalAddress does not conform to Address',
+ }),
+ feeCosts: z
+ .array(
+ z.object({
+ name: z.string(),
+ description: z.string(),
+ percentage: z.string(),
+ token: crossChainTokenSchema,
+ amount: z.string().transform((amount) => BigInt(amount)),
+ amountUSD: z.string(),
+ included: z.boolean(),
+ }),
+ )
+ .default([]),
+ gasCosts: z.array(
+ z.object({
+ type: z.enum(['SUM', 'APPROVE', 'SEND']),
+ price: z.string(),
+ estimate: z.string(),
+ limit: z.string(),
+ amount: z.string().transform((amount) => BigInt(amount)),
+ amountUSD: z.string(),
+ token: crossChainTokenSchema,
+ }),
+ ),
+ executionDuration: z.number(),
+})
+
+export const crossChainToolDetailsSchema = z.object({
+ key: z.string(),
+ name: z.string(),
+ logoURI: z.string(),
+})
+
+export const crossChainTransactionRequestSchema = z.object({
+ chainId: z.number().refine((chainId) => isXSwapSupportedChainId(chainId), {
+ message: `chainId must exist in XSwapChainId`,
+ }),
+ data: z.string().refine((data) => isHex(data), {
+ message: 'data does not conform to Hex',
+ }),
+ from: z.string().refine((from) => isAddress(from), {
+ message: 'from does not conform to Address',
+ }),
+ gasLimit: z
+ .string()
+ .refine((gasLimit) => isHex(gasLimit), {
+ message: 'gasLimit does not conform to Hex',
+ })
+ .transform((gasLimit) => hexToBigInt(gasLimit)),
+ gasPrice: z
+ .string()
+ .refine((gasPrice) => isHex(gasPrice), {
+ message: 'gasPrice does not conform to Hex',
+ })
+ .transform((gasPrice) => hexToBigInt(gasPrice)),
+ to: z.string().refine((to) => isAddress(to), {
+ message: 'to does not conform to Address',
+ }),
+ value: z
+ .string()
+ .refine((value) => isHex(value), {
+ message: 'value does not conform to Hex',
+ })
+ .transform((value) => hexToBigInt(value)),
+})
+
+const _crossChainStepSchema = z.object({
+ id: z.string(),
+ type: z.enum(['swap', 'cross', 'lifi']),
+ tool: z.string(),
+ toolDetails: crossChainToolDetailsSchema,
+ action: crossChainActionSchema,
+ estimate: crossChainEstimateSchema,
+ transactionRequest: crossChainTransactionRequestSchema.optional(),
+})
+
+export const crossChainStepSchema = _crossChainStepSchema.extend({
+ includedSteps: z.array(_crossChainStepSchema),
+})
+
+export const crossChainRouteSchema = z.object({
+ id: z.string(),
+ fromChainId: z.coerce
+ .number()
+ .refine((chainId) => isXSwapSupportedChainId(chainId), {
+ message: `fromChainId must exist in XSwapChainId`,
+ }),
+ fromAmount: z.string().transform((amount) => BigInt(amount)),
+ fromToken: crossChainTokenSchema,
+ toChainId: z.coerce
+ .number()
+ .refine((chainId) => isXSwapSupportedChainId(chainId), {
+ message: `toChainId must exist in XSwapChainId`,
+ }),
+ toAmount: z.string().transform((amount) => BigInt(amount)),
+ toAmountMin: z.string().transform((amount) => BigInt(amount)),
+ toToken: crossChainTokenSchema,
+ gasCostUSD: z.string(),
+ steps: z.array(crossChainStepSchema),
+ tags: z.array(z.string()).optional(),
+ transactionRequest: crossChainTransactionRequestSchema.optional(),
+})
diff --git a/apps/web/src/lib/swap/cross-chain/types.ts b/apps/web/src/lib/swap/cross-chain/types.ts
new file mode 100644
index 0000000000..9111754dbb
--- /dev/null
+++ b/apps/web/src/lib/swap/cross-chain/types.ts
@@ -0,0 +1,35 @@
+import { z } from 'zod'
+import {
+ crossChainActionSchema,
+ crossChainEstimateSchema,
+ crossChainRouteSchema,
+ crossChainStepSchema,
+ crossChainToolDetailsSchema,
+ crossChainTransactionRequestSchema,
+} from './schema'
+
+type CrossChainAction = z.infer
+
+type CrossChainEstimate = z.infer
+
+type CrossChainRoute = z.infer
+
+type CrossChainStep = z.infer
+
+type CrossChainToolDetails = z.infer
+
+type CrossChainTransactionRequest = z.infer<
+ typeof crossChainTransactionRequestSchema
+>
+
+type CrossChainRouteOrder = 'CHEAPEST' | 'FASTEST'
+
+export type {
+ CrossChainAction,
+ CrossChainEstimate,
+ CrossChainRoute,
+ CrossChainStep,
+ CrossChainToolDetails,
+ CrossChainTransactionRequest,
+ CrossChainRouteOrder,
+}
diff --git a/apps/web/src/lib/swap/cross-chain/utils.tsx b/apps/web/src/lib/swap/cross-chain/utils.tsx
new file mode 100644
index 0000000000..f14fbd64b2
--- /dev/null
+++ b/apps/web/src/lib/swap/cross-chain/utils.tsx
@@ -0,0 +1,79 @@
+import { ChainId } from 'sushi/chain'
+import { Amount, Native, Token, Type } from 'sushi/currency'
+import { zeroAddress } from 'viem'
+import { CrossChainStep } from './types'
+
+interface FeeBreakdown {
+ amount: Amount
+ amountUSD: number
+}
+
+export interface FeesBreakdown {
+ gas: Map
+ protocol: Map
+}
+
+enum FeeType {
+ GAS = 'GAS',
+ PROTOCOL = 'PROTOCOL',
+}
+
+export const getCrossChainFeesBreakdown = (route: CrossChainStep[]) => {
+ const gasFeesBreakdown = getFeesBreakdown(route, FeeType.GAS)
+ const protocolFeesBreakdown = getFeesBreakdown(route, FeeType.PROTOCOL)
+ const gasFeesUSD = Array.from(gasFeesBreakdown.values()).reduce(
+ (sum, gasCost) => sum + gasCost.amountUSD,
+ 0,
+ )
+ const protocolFeesUSD = Array.from(protocolFeesBreakdown.values()).reduce(
+ (sum, feeCost) => sum + feeCost.amountUSD,
+ 0,
+ )
+ const totalFeesUSD = gasFeesUSD + protocolFeesUSD
+
+ return {
+ feesBreakdown: {
+ gas: gasFeesBreakdown,
+ protocol: protocolFeesBreakdown,
+ },
+ totalFeesUSD,
+ gasFeesUSD,
+ protocolFeesUSD,
+ }
+}
+
+const getFeesBreakdown = (route: CrossChainStep[], feeType: FeeType) => {
+ return route.reduce((feesByChainId, step) => {
+ const fees =
+ feeType === FeeType.PROTOCOL
+ ? step.estimate.feeCosts.filter((fee) => fee.included === false)
+ : step.estimate.gasCosts
+
+ if (fees.length === 0) return feesByChainId
+
+ const token =
+ fees[0].token.address === zeroAddress
+ ? Native.onChain(fees[0].token.chainId)
+ : new Token(fees[0].token)
+
+ const { amount, amountUSD } = fees.reduce(
+ (acc, feeCost) => {
+ const amount = Amount.fromRawAmount(token, feeCost.amount)
+
+ acc.amount = acc.amount.add(amount)
+ acc.amountUSD += +feeCost.amountUSD
+ return acc
+ },
+ { amount: Amount.fromRawAmount(token, 0), amountUSD: 0 },
+ )
+
+ const feeByChainId = feesByChainId.get(amount.currency.chainId)
+
+ feesByChainId.set(amount.currency.chainId, {
+ amount: feeByChainId ? feeByChainId.amount.add(amount) : amount,
+ amountUSD: feeByChainId ? feeByChainId.amountUSD + amountUSD : amountUSD,
+ })
+
+ return feesByChainId
+ }, new Map())
+}
diff --git a/apps/web/src/lib/swap/queryParamsSchema.ts b/apps/web/src/lib/swap/queryParamsSchema.ts
deleted file mode 100644
index 23e2120abb..0000000000
--- a/apps/web/src/lib/swap/queryParamsSchema.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Address, isAddress } from 'viem'
-import { z } from 'zod'
-
-import { SwapChainId } from '../../types'
-
-export const queryParamsSchema = z.object({
- chainId: z.coerce
- .number()
- .int()
- .gte(0)
- .lte(2 ** 256)
- .optional()
- .transform((chainId) => chainId as SwapChainId | undefined),
- fromChainId: z.coerce
- .number()
- .int()
- .gte(0)
- .lte(2 ** 256)
- .optional()
- .transform((chainId) => chainId as SwapChainId | undefined),
- // .refine(
- // (chainId) =>
- // isUniswapV2FactoryChainId(chainId) ||
- // isConstantProductPoolFactoryChainId(chainId) ||
- // isStablePoolFactoryChainId(chainId),
- // {
- // message: 'ChainId not supported.',
- // }
- // ),
- // fromCurrency: z
- // .string()
- // .nullable()
- // .transform((arg) => (arg ? arg : 'NATIVE')),
- fromCurrency: z
- .nullable(z.string())
- .transform((currency) =>
- typeof currency === 'string' ? currency : 'NATIVE',
- ),
- toChainId: z.coerce
- .number()
- .int()
- .gte(0)
- .lte(2 ** 256)
- .optional()
- .transform((chainId) => chainId as SwapChainId | undefined),
- toCurrency: z
- .nullable(z.string())
- .transform((currency) => (typeof currency === 'string' ? currency : '')),
- // .transform((currency) => (typeof currency === 'string' ? currency : 'NATIVE')),
- // toCurrency: z
- // .string()
- // .nullable()
- // .transform((arg) => (arg ? arg : 'SUSHI')),
- amount: z.optional(z.nullable(z.string())).transform((val) => val ?? ''),
- recipient: z.optional(
- z
- .nullable(z.string())
- .transform((val) => (val && isAddress(val) ? (val as Address) : null)),
- ),
- review: z.optional(z.nullable(z.boolean())),
-})
-// .transform((val) => ({
-// ...val,
-// toCurrency: defaultQuoteCurrency[val.fromChainId].wrapped.address,
-// }))
diff --git a/apps/web/src/lib/wagmi/components/web3-input/Currency/CurrencyInput.tsx b/apps/web/src/lib/wagmi/components/web3-input/Currency/CurrencyInput.tsx
index eb1412ca6a..0d62bcf8de 100644
--- a/apps/web/src/lib/wagmi/components/web3-input/Currency/CurrencyInput.tsx
+++ b/apps/web/src/lib/wagmi/components/web3-input/Currency/CurrencyInput.tsx
@@ -1,7 +1,15 @@
'use client'
+import { ChevronRightIcon } from '@heroicons/react/24/outline'
import { useIsMounted } from '@sushiswap/hooks'
-import { Badge, Button, SelectIcon, TextField, classNames } from '@sushiswap/ui'
+import {
+ Badge,
+ Button,
+ SelectIcon,
+ SelectPrimitive,
+ TextField,
+ classNames,
+} from '@sushiswap/ui'
import { Currency } from '@sushiswap/ui'
import { SkeletonBox } from '@sushiswap/ui'
import { NetworkIcon } from '@sushiswap/ui/icons/NetworkIcon'
@@ -13,7 +21,7 @@ import {
useState,
useTransition,
} from 'react'
-import { ChainId } from 'sushi/chain'
+import { Chain, ChainId } from 'sushi/chain'
import { Token, Type, tryParseAmount } from 'sushi/currency'
import { Percent } from 'sushi/math'
import { useAccount } from 'wagmi'
@@ -165,14 +173,15 @@ const CurrencyInput: FC = ({
id={id}
type="button"
className={classNames(
- currency ? 'pl-2 pr-3 text-xl' : '',
+ currency ? 'pl-2 pr-3' : '',
+ networks ? '!h-11' : '',
'!rounded-full data-[state=inactive]:hidden data-[state=active]:flex',
)}
>
{currency ? (
- <>
-
- {networks ? (
+ networks ? (
+ <>
+
= ({
/>
}
>
-
+
- ) : (
+
+
+ {currency.symbol}
+
+ {Chain.from(currency.chainId)?.name}
+
+
+
+
+
+ >
+ ) : (
+ <>
+
- )}
-
- {currency.symbol}
-
- >
+
+ {currency.symbol}
+
+ >
+ )
) : (
'Select token'
)}
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx
index d8cb59ea85..9674c15112 100644
--- a/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx
@@ -13,7 +13,7 @@ import {
SUSHISWAP_V3_POSTIION_MANAGER,
isSushiSwapV3ChainId,
} from 'sushi/config'
-import { Amount, Type, unwrapToken } from 'sushi/currency'
+import { Amount, Type } from 'sushi/currency'
import { NonfungiblePositionManager, Position } from 'sushi/pool/sushiswap-v3'
import { Hex, SendTransactionReturnType, UserRejectedRequestError } from 'viem'
import {
@@ -69,16 +69,14 @@ export const ConcentratedLiquidityCollectButton: FC<
? Amount.fromRawAmount(token0, positionDetails.fees[0])
: undefined
const feeValue1 = positionDetails.fees
- ? Amount.fromRawAmount(token0, positionDetails.fees[1])
+ ? Amount.fromRawAmount(token1, positionDetails.fees[1])
: undefined
const { calldata, value } =
NonfungiblePositionManager.collectCallParameters({
tokenId: positionDetails.tokenId.toString(),
- expectedCurrencyOwed0:
- feeValue0 ?? Amount.fromRawAmount(unwrapToken(token0), 0),
- expectedCurrencyOwed1:
- feeValue1 ?? Amount.fromRawAmount(unwrapToken(token1), 0),
+ expectedCurrencyOwed0: feeValue0 ?? Amount.fromRawAmount(token0, 0),
+ expectedCurrencyOwed1: feeValue1 ?? Amount.fromRawAmount(token1, 0),
recipient: account,
})
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx
new file mode 100644
index 0000000000..3b207f9c4e
--- /dev/null
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx
@@ -0,0 +1,124 @@
+'use client'
+
+import {
+ CardContent,
+ CardCurrencyAmountItem,
+ CardFooter,
+ CardGroup,
+ CardLabel,
+ Switch,
+} from '@sushiswap/ui'
+import { Button } from '@sushiswap/ui'
+import { FC, useMemo, useState } from 'react'
+import { ConcentratedLiquidityPosition } from 'src/lib/wagmi/hooks/positions/types'
+import { Checker } from 'src/lib/wagmi/systems/Checker'
+import { Address, ChainId, Position } from 'sushi'
+import { Amount, Native, Type, unwrapToken } from 'sushi/currency'
+import { formatUSD } from 'sushi/format'
+import { ConcentratedLiquidityCollectButton } from './ConcentratedLiquidityCollectButton'
+
+interface ConcentratedLiquidityCollectWidget {
+ position: Position | undefined
+ positionDetails: ConcentratedLiquidityPosition | undefined
+ token0: Type | undefined
+ token1: Type | undefined
+ chainId: ChainId
+ isLoading: boolean
+ address: Address | undefined
+ amounts: undefined[] | Amount[]
+ fiatValuesAmounts: number[]
+}
+
+export const ConcentratedLiquidityCollectWidget: FC<
+ ConcentratedLiquidityCollectWidget
+> = ({
+ position,
+ positionDetails,
+ token0,
+ token1,
+ chainId,
+ isLoading,
+ address,
+ amounts,
+ fiatValuesAmounts,
+}) => {
+ const [receiveWrapped, setReceiveWrapped] = useState(false)
+ const nativeToken = useMemo(() => Native.onChain(chainId), [chainId])
+
+ const positionHasNativeToken = useMemo(() => {
+ if (!nativeToken || !token0 || !token1) return false
+ return (
+ token0.isNative ||
+ token1.isNative ||
+ token0.address === nativeToken?.wrapped?.address ||
+ token1.address === nativeToken?.wrapped?.address
+ )
+ }, [token0, token1, nativeToken])
+
+ const expectedAmount0 = useMemo(() => {
+ const expectedToken0 =
+ !token0 || receiveWrapped ? token0?.wrapped : unwrapToken(token0)
+ if (amounts[0] === undefined || !expectedToken0) return undefined
+ return Amount.fromRawAmount(expectedToken0, amounts[0].quotient)
+ }, [token0, receiveWrapped, amounts])
+
+ const expectedAmount1 = useMemo(() => {
+ const expectedToken1 =
+ !token1 || receiveWrapped ? token1?.wrapped : unwrapToken(token1)
+ if (amounts[1] === undefined || !expectedToken1) return undefined
+ return Amount.fromRawAmount(expectedToken1, amounts[1].quotient)
+ }, [token1, receiveWrapped, amounts])
+
+ return (
+ <>
+
+
+ Tokens
+
+
+
+ {positionHasNativeToken ? (
+
+
+ {`Receive ${nativeToken.wrapped.symbol} instead of ${nativeToken.symbol}`}
+
+
+
+ ) : null}
+
+
+
+ {({ send, isPending }) => (
+
+
+
+
+
+ )}
+
+
+ >
+ )
+}
diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx
index ae4ea11ac4..dc1bd826eb 100644
--- a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx
+++ b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx
@@ -35,6 +35,7 @@ import {
List,
SettingsModule,
SettingsOverlay,
+ Switch,
classNames,
} from '@sushiswap/ui'
import { Button } from '@sushiswap/ui'
@@ -52,7 +53,7 @@ import {
SushiSwapV3ChainId,
isSushiSwapV3ChainId,
} from 'sushi/config'
-import { Amount, Type, unwrapToken } from 'sushi/currency'
+import { Amount, Native, Type, unwrapToken } from 'sushi/currency'
import { Percent, ZERO } from 'sushi/math'
import { NonfungiblePositionManager, Position } from 'sushi/pool/sushiswap-v3'
import { Hex, SendTransactionReturnType, UserRejectedRequestError } from 'viem'
@@ -90,6 +91,7 @@ export const ConcentratedLiquidityRemoveWidget: FC<
const { chain } = useAccount()
const client = usePublicClient()
const [value, setValue] = useState('0')
+ const [receiveWrapped, setReceiveWrapped] = useState(false)
const [slippageTolerance] = useSlippageTolerance(
SlippageToleranceStorageKey.RemoveLiquidity,
)
@@ -161,20 +163,40 @@ export const ConcentratedLiquidityRemoveWidget: FC<
}
}, [])
+ const [expectedToken0, expectedToken1] = useMemo(() => {
+ const expectedToken0 =
+ !token0 || receiveWrapped ? token0?.wrapped : unwrapToken(token0)
+ const expectedToken1 =
+ !token1 || receiveWrapped ? token1?.wrapped : unwrapToken(token1)
+ return [expectedToken0, expectedToken1]
+ }, [token0, token1, receiveWrapped])
+
const [feeValue0, feeValue1] = useMemo(() => {
- if (positionDetails && token0 && token1) {
+ if (positionDetails && expectedToken0 && expectedToken1) {
const feeValue0 = positionDetails.fees
- ? Amount.fromRawAmount(token0, positionDetails.fees[0])
+ ? Amount.fromRawAmount(expectedToken0, positionDetails.fees[0])
: undefined
const feeValue1 = positionDetails.fees
- ? Amount.fromRawAmount(token1, positionDetails.fees[1])
+ ? Amount.fromRawAmount(expectedToken1, positionDetails.fees[1])
: undefined
return [feeValue0, feeValue1]
}
return [undefined, undefined]
- }, [positionDetails, token0, token1])
+ }, [positionDetails, expectedToken0, expectedToken1])
+
+ const nativeToken = useMemo(() => Native.onChain(chainId), [chainId])
+
+ const positionHasNativeToken = useMemo(() => {
+ if (!nativeToken || !token0 || !token1) return false
+ return (
+ token0.isNative ||
+ token1.isNative ||
+ token0.address === nativeToken?.wrapped?.address ||
+ token1.address === nativeToken?.wrapped?.address
+ )
+ }, [token0, token1, nativeToken])
const prepare = useMemo(() => {
const liquidityPercentage = new Percent(debouncedValue, 100)
@@ -186,17 +208,17 @@ export const ConcentratedLiquidityRemoveWidget: FC<
: undefined
const liquidityValue0 =
- token0 && typeof discountedAmount0 === 'bigint'
- ? Amount.fromRawAmount(unwrapToken(token0), discountedAmount0)
+ expectedToken0 && typeof discountedAmount0 === 'bigint'
+ ? Amount.fromRawAmount(expectedToken0, discountedAmount0)
: undefined
const liquidityValue1 =
- token1 && typeof discountedAmount1 === 'bigint'
- ? Amount.fromRawAmount(unwrapToken(token1), discountedAmount1)
+ expectedToken1 && typeof discountedAmount1 === 'bigint'
+ ? Amount.fromRawAmount(expectedToken1, discountedAmount1)
: undefined
if (
- token0 &&
- token1 &&
+ expectedToken0 &&
+ expectedToken1 &&
position &&
account &&
positionDetails &&
@@ -250,9 +272,9 @@ export const ConcentratedLiquidityRemoveWidget: FC<
position,
positionDetails,
slippageTolerance,
- token0,
- token1,
debouncedValue,
+ expectedToken0,
+ expectedToken1,
])
const { isError: isSimulationError } = useCall({
@@ -501,10 +523,7 @@ export const ConcentratedLiquidityRemoveWidget: FC<
{position?.amount0 && (
-
+
${fiatAmountsAsNumber[0].toFixed(2)}
@@ -533,10 +552,7 @@ export const ConcentratedLiquidityRemoveWidget: FC<
)}
{position?.amount1 && (
-
+
${fiatAmountsAsNumber[1].toFixed(2)}
@@ -566,6 +582,17 @@ export const ConcentratedLiquidityRemoveWidget: FC<
)}
+ {positionHasNativeToken ? (
+
+
+ {`Receive ${nativeToken.wrapped.symbol} instead of ${nativeToken.symbol}`}
+
+
+
+ ) : null}
{' '}
-
>
)
}
if (dialogState.dest === StepState.PartialSuccess) {
+ const fromTokenSymbol =
+ routeRef?.current?.steps?.[0]?.includedSteps?.[1]?.type === 'swap'
+ ? routeRef?.current?.steps?.[0]?.includedSteps?.[1]?.action?.fromToken
+ ?.symbol
+ : routeRef?.current?.steps?.[0]?.includedSteps?.[2]?.type === 'swap'
+ ? routeRef?.current?.steps?.[0]?.includedSteps?.[2]?.action?.fromToken
+ ?.symbol
+ : undefined
+
return (
<>
- We {`couldn't`} swap {tradeRef?.current?.dstBridgeToken?.symbol} into{' '}
- {token1?.symbol}, {tradeRef?.current?.dstBridgeToken?.symbol} has been
- send to{' '}
+ We {`couldn't`} swap {fromTokenSymbol} into {token1?.symbol},{' '}
+ {fromTokenSymbol} has been send to{' '}
{recipient ? (
= ({
return
}
-const CrossChainAdapter = ({
- adapter,
-}: { adapter: SushiXSwap2Adapter | undefined }) => {
- return (
-
- powered by{' '}
- {adapter === SushiXSwap2Adapter.Stargate ? (
- <>
-
-
-
{' '}
- Stargate
- >
- ) : (
- <>
-
-
-
{' '}
- Squid
- >
- )}
-
- )
-}
-
export enum StepState {
Sign = 0,
NotStarted = 1,
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-fees-hover-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-fees-hover-card.tsx
new file mode 100644
index 0000000000..50bd6b4b20
--- /dev/null
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-fees-hover-card.tsx
@@ -0,0 +1,140 @@
+import {
+ Card,
+ CardContent,
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@sushiswap/ui'
+import { FC, useMemo } from 'react'
+import { FeesBreakdown } from 'src/lib/swap/cross-chain'
+import { ChainId } from 'sushi/chain'
+import { formatNumber, formatUSD } from 'sushi/format'
+
+interface CrossChainSwapFeesHoverCardProps {
+ feesBreakdown: FeesBreakdown
+ gasFeesUSD: number
+ protocolFeesUSD: number
+ chainId0: ChainId
+ chainId1: ChainId
+ children: React.ReactNode
+}
+
+export const CrossChainSwapFeesHoverCard: FC =
+ ({ feesBreakdown, chainId0, chainId1, children }) => {
+ const content = useMemo(() => {
+ return (
+
+
+ {feesBreakdown.gas.size > 0 ? (
+
+
Network Fees
+
+ {feesBreakdown.gas.get(chainId0) ? (
+
+
+ {formatNumber(
+ feesBreakdown.gas.get(chainId0)!.amount.toExact(),
+ )}{' '}
+ {
+ feesBreakdown.gas.get(chainId0)!.amount.currency
+ .symbol
+ }
+ {' '}
+ ({formatUSD(feesBreakdown.gas.get(chainId0)!.amountUSD)})
+
+ ) : null}
+ {feesBreakdown.gas.get(chainId1) ? (
+
+
+ {formatNumber(
+ feesBreakdown.gas.get(chainId1)!.amount.toExact(),
+ )}{' '}
+ {
+ feesBreakdown.gas.get(chainId1)!.amount.currency
+ .symbol
+ }
+ {' '}
+ ({formatUSD(feesBreakdown.gas.get(chainId1)!.amountUSD)})
+
+ ) : null}
+
+
+ ) : null}
+ {feesBreakdown.protocol.size > 0 ? (
+
+
Protocol Fees
+
+ {feesBreakdown.protocol.get(chainId0) ? (
+
+
+ {formatNumber(
+ feesBreakdown.protocol
+ .get(chainId0)!
+ .amount.toExact(),
+ )}{' '}
+ {
+ feesBreakdown.protocol.get(chainId0)!.amount.currency
+ .symbol
+ }
+ {' '}
+ (
+ {formatUSD(
+ feesBreakdown.protocol.get(chainId0)!.amountUSD,
+ )}
+ )
+
+ ) : null}
+ {feesBreakdown.protocol.get(chainId1) ? (
+
+
+ {formatNumber(
+ feesBreakdown.protocol
+ .get(chainId1)!
+ .amount.toExact(),
+ )}{' '}
+ {
+ feesBreakdown.protocol.get(chainId1)!.amount.currency
+ .symbol
+ }
+ {' '}
+ (
+ {formatUSD(
+ feesBreakdown.protocol.get(chainId1)!.amountUSD,
+ )}
+ )
+
+ ) : null}
+
+
+ ) : null}
+
+
+ )
+ }, [feesBreakdown, chainId0, chainId1])
+
+ return (
+ <>
+
+
+ {children}
+
+ {content}
+
+
+
+
+
+ e.stopPropagation()} asChild>
+ {children}
+
+
+ {content}
+
+
+
+ >
+ )
+ }
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx
new file mode 100644
index 0000000000..203bb65aaf
--- /dev/null
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx
@@ -0,0 +1,193 @@
+import { ClockIcon } from '@heroicons/react/24/outline'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+ Separator,
+ SkeletonText,
+ classNames,
+} from '@sushiswap/ui'
+import { GasIcon } from '@sushiswap/ui/icons/GasIcon'
+import React, { FC, useMemo } from 'react'
+import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain'
+import type {
+ CrossChainRoute as CrossChainRouteType,
+ CrossChainRouteOrder,
+} from 'src/lib/swap/cross-chain/types'
+import { Amount } from 'sushi/currency'
+import { formatUSD } from 'sushi/format'
+import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price'
+import { CrossChainSwapFeesHoverCard } from './cross-chain-swap-fees-hover-card'
+import { CrossChainSwapRouteView } from './cross-chain-swap-route-view'
+import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider'
+
+interface CrossChainSwapRouteCardProps {
+ route: CrossChainRouteType
+ order: CrossChainRouteOrder
+ isSelected: boolean
+ onSelect: () => void
+}
+
+export const CrossChainSwapRouteCard: FC = ({
+ route,
+ order,
+ isSelected,
+ onSelect,
+}) => {
+ const {
+ state: { token1, chainId0, chainId1 },
+ } = useDerivedStateCrossChainSwap()
+
+ const { data: price } = usePrice({
+ chainId: token1?.chainId,
+ address: token1?.wrapped.address,
+ })
+
+ const amountOut = useMemo(
+ () =>
+ route?.toAmount && token1
+ ? Amount.fromRawAmount(token1, route.toAmount)
+ : undefined,
+ [token1, route?.toAmount],
+ )
+
+ const amountOutUSD = useMemo(
+ () =>
+ price && amountOut
+ ? `${(
+ (price * Number(amountOut.quotient)) /
+ 10 ** amountOut.currency.decimals
+ ).toFixed(2)}`
+ : undefined,
+ [amountOut, price],
+ )
+
+ const {
+ step,
+ executionDuration,
+ feesBreakdown,
+ gasFeesUSD,
+ protocolFeesUSD,
+ totalFeesUSD,
+ } = useMemo(() => {
+ const step = route.steps[0]
+ const executionDurationSeconds = step.estimate.executionDuration
+ const executionDurationMinutes = Math.floor(executionDurationSeconds / 60)
+
+ const executionDuration =
+ executionDurationSeconds < 60
+ ? `${executionDurationSeconds} seconds`
+ : `${executionDurationMinutes} minutes`
+
+ const { feesBreakdown, totalFeesUSD, gasFeesUSD, protocolFeesUSD } =
+ getCrossChainFeesBreakdown(route.steps)
+
+ return {
+ step,
+ executionDuration,
+ feesBreakdown,
+ totalFeesUSD,
+ gasFeesUSD,
+ protocolFeesUSD,
+ }
+ }, [route?.steps])
+
+ return (
+
+
+
+
+
+ {amountOut && token1 ? (
+ `${amountOut?.toSignificant(6)} ${token1?.symbol}`
+ ) : (
+
+
+
+ )}
+
+
+ {amountOutUSD ? (
+ {`≈ ${amountOutUSD} after fees`}
+ ) : (
+
+
+
+ )}
+
+
+
+ {route.tags?.includes(order) ? (
+
+
+ {order === 'CHEAPEST' ? 'Best Return' : 'Fastest'}
+
+
+ ) : route.tags?.includes(
+ order === 'FASTEST' ? 'CHEAPEST' : 'FASTEST',
+ ) ? (
+
+
+ {order === 'FASTEST' ? 'Best Return' : 'Fastest'}
+
+
+ ) : null}
+
+
+
+ {isSelected && step.includedSteps.length > 1 ? (
+ <>
+
+
+
+
+
+ >
+ ) : null}
+
+
+
+
+
+ {executionDuration}
+
+
+
+
+ {formatUSD(totalFeesUSD)}
+
+
+
+
+
+
+ {step.toolDetails.name}
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx
new file mode 100644
index 0000000000..429ae5d56f
--- /dev/null
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx
@@ -0,0 +1,185 @@
+import { ClockIcon } from '@heroicons/react/24/outline'
+import { Card, SkeletonCircle, SkeletonText, classNames } from '@sushiswap/ui'
+import { GasIcon } from '@sushiswap/ui/icons/GasIcon'
+import React, { FC, useMemo } from 'react'
+import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain'
+import type {
+ CrossChainRoute as CrossChainRouteType,
+ CrossChainRouteOrder,
+} from 'src/lib/swap/cross-chain/types'
+import { Amount } from 'sushi/currency'
+import { formatUSD } from 'sushi/format'
+import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price'
+import { CrossChainSwapFeesHoverCard } from './cross-chain-swap-fees-hover-card'
+import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider'
+
+interface CrossChainSwapRouteMobileCardProps {
+ route: CrossChainRouteType | undefined
+ order: CrossChainRouteOrder
+ isSelected: boolean
+ onSelect?: () => void
+}
+
+export const CrossChainSwapRouteMobileCard: FC<
+ CrossChainSwapRouteMobileCardProps
+> = ({ route, order, isSelected, onSelect }) => {
+ const {
+ state: { token1, chainId0, chainId1 },
+ } = useDerivedStateCrossChainSwap()
+
+ const { data: price } = usePrice({
+ chainId: token1?.chainId,
+ address: token1?.wrapped.address,
+ })
+
+ const amountOut = useMemo(
+ () =>
+ route?.toAmount && token1
+ ? Amount.fromRawAmount(token1, route.toAmount)
+ : undefined,
+ [token1, route?.toAmount],
+ )
+
+ const amountOutUSD = useMemo(
+ () =>
+ price && amountOut
+ ? `${(
+ (price * Number(amountOut.quotient)) /
+ 10 ** amountOut.currency.decimals
+ ).toFixed(2)}`
+ : undefined,
+ [amountOut, price],
+ )
+
+ const {
+ step,
+ executionDuration,
+ feesBreakdown,
+ gasFeesUSD,
+ protocolFeesUSD,
+ totalFeesUSD,
+ } = useMemo(() => {
+ const step = route?.steps[0]
+ if (!step)
+ return {
+ step,
+ executionDuration: undefined,
+ feesBreakdown: undefined,
+ gasFeesUSD: undefined,
+ protocolFeesUSD: undefined,
+ totalFeesUSD: undefined,
+ }
+
+ const executionDurationSeconds = step.estimate.executionDuration
+ const executionDurationMinutes = Math.floor(executionDurationSeconds / 60)
+
+ const executionDuration =
+ executionDurationSeconds < 60
+ ? `${executionDurationSeconds} seconds`
+ : `${executionDurationMinutes} minutes`
+
+ const { feesBreakdown, totalFeesUSD, gasFeesUSD, protocolFeesUSD } =
+ getCrossChainFeesBreakdown(route.steps)
+
+ return {
+ step,
+ executionDuration,
+ feesBreakdown,
+ totalFeesUSD,
+ gasFeesUSD,
+ protocolFeesUSD,
+ }
+ }, [route?.steps])
+
+ return (
+
+
+ {step ? (
+

+ ) : (
+
+ )}
+
+
+ {amountOut && token1 ? (
+
+ {amountOut?.toSignificant(6)} {token1?.symbol}
+
+ ) : (
+
+
+
+ )}
+ {amountOutUSD ? (
+
+ ≈ ${amountOutUSD} after fees
+
+ ) : (
+
+
+
+ )}
+ {route?.tags?.includes(order) ? (
+
+
+ {order === 'CHEAPEST' ? 'Best Return' : 'Fastest'}
+
+
+ ) : route?.tags?.includes(
+ order === 'FASTEST' ? 'CHEAPEST' : 'FASTEST',
+ ) ? (
+
+
+ {order === 'FASTEST' ? 'Best Return' : 'Fastest'}
+
+
+ ) : null}
+
+
+ {executionDuration ? (
+
+
+ {executionDuration}
+
+ ) : (
+
+
+
+ )}
+ {feesBreakdown ? (
+
+
+
+ {formatUSD(totalFeesUSD)}
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx
new file mode 100644
index 0000000000..22672c080d
--- /dev/null
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx
@@ -0,0 +1,353 @@
+'use client'
+
+import { ChevronRightIcon } from '@heroicons/react/24/outline'
+import { useBreakpoint, useMediaQuery } from '@sushiswap/hooks'
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ cloudinaryFetchLoader,
+} from '@sushiswap/ui'
+import { QueryStatus } from '@tanstack/react-query'
+import Image from 'next/image'
+import { FC, useId, useState } from 'react'
+import { CrossChainRoute, CrossChainRouteOrder } from 'src/lib/swap/cross-chain'
+import { useSidebar } from 'src/ui/sidebar'
+import { CrossChainSwapRouteCard } from './cross-chain-swap-route-card'
+import { CrossChainSwapRouteMobileCard } from './cross-chain-swap-route-mobile-card'
+import {
+ useCrossChainTradeRoutes,
+ useDerivedStateCrossChainSwap,
+} from './derivedstate-cross-chain-swap-provider'
+
+export const CrossChainSwapRouteSelector = () => {
+ const { data: routes, status } = useCrossChainTradeRoutes()
+ const { isOpen: isSidebarOpen } = useSidebar()
+
+ const {
+ state: { routeOrder, selectedBridge },
+ mutate: { setRouteOrder, setSelectedBridge },
+ } = useDerivedStateCrossChainSwap()
+
+ const isLg = useMediaQuery({
+ query: `(min-width: 1056px)`,
+ })
+ const { isXl } = useBreakpoint('xl')
+
+ const showDesktopSelector = isSidebarOpen ? isXl : isLg
+
+ return showDesktopSelector ? (
+
+ ) : (
+
+ )
+}
+
+interface RouteSelectorProps {
+ routeOrder: CrossChainRouteOrder
+ setRouteOrder: (order: CrossChainRouteOrder) => void
+ selectedBridge: string | undefined
+ setSelectedBridge: (bridge: string | undefined) => void
+ routes: CrossChainRoute[] | undefined
+ status: QueryStatus
+}
+
+const DesktopRouteSelector: FC = ({
+ routeOrder,
+ setRouteOrder,
+ routes,
+ selectedBridge,
+ setSelectedBridge,
+ status,
+}) => {
+ return (
+
+ {status === 'success' && routes && routes.length > 0 ? (
+ <>
+
+
+
Select A Route
+
+
+
+
+
+
+
+ {routes.map((route) => (
+ setSelectedBridge(route.steps[0].tool)}
+ />
+ ))}
+
+ >
+ ) : status === 'error' || routes?.length === 0 ? (
+ <>
+
+ Select A Route
+
+
+
+
+
+ No Routes Found.
+
+ The problem might be due to high transaction fees, not enough
+ liquidity, the amount being too small, or no available routes for
+ the combination you selected.
+
+
+ >
+ ) : (
+ <>
+
+
+ SushiXSwap Aggregator
+
+
+ Swap Anything On Any Chain
+
+
+
+
+
+
+
+ ✨ Best Pricing
+ 🍣 Fastest Response Time
+
+ 👨🍳 Widest Network Coverage
+
+ >
+ )}
+
+ )
+}
+
+const MobileRouteSelector: FC = ({
+ routeOrder,
+ setRouteOrder,
+ routes,
+ selectedBridge,
+ setSelectedBridge,
+ status,
+}) => {
+ const [dialogOpen, setDialogOpen] = useState(false)
+
+ return (
+
+ )
+}
+
+const SUSHI_IMGS = [
+ 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-0.png',
+ 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-1.png',
+ 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-2.png',
+ 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-3.png',
+ 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-4.png',
+ 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-5.png',
+ 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-6.png',
+]
+
+const DUP_SUSHI_IMAGES = [...SUSHI_IMGS, ...SUSHI_IMGS]
+
+const RouteSelectorMarquee = () => {
+ return (
+
+
+ {DUP_SUSHI_IMAGES.map((img, i) => (
+
+ ))}
+
+
+ )
+}
+
+const NoRoutesFound = () => {
+ const id = useId()
+
+ return (
+
+ )
+}
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx
new file mode 100644
index 0000000000..093bf65995
--- /dev/null
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx
@@ -0,0 +1,233 @@
+import { classNames } from '@sushiswap/ui'
+import { NetworkIcon } from '@sushiswap/ui/icons/NetworkIcon'
+import React, { useMemo } from 'react'
+import { FC } from 'react'
+import type {
+ CrossChainAction,
+ CrossChainEstimate,
+ CrossChainStep,
+ CrossChainToolDetails,
+} from 'src/lib/swap/cross-chain/types'
+import { Chain } from 'sushi/chain'
+import { Amount, Native, Token, Type } from 'sushi/currency'
+import { formatNumber } from 'sushi/format'
+import { zeroAddress } from 'viem'
+
+interface CrossChainSwapRouteViewProps {
+ step: CrossChainStep
+}
+
+export const CrossChainSwapRouteView: FC = ({
+ step,
+}) => {
+ const { srcStep, bridgeStep, dstStep } = useMemo(() => {
+ const bridgeIndex = step.includedSteps.findIndex(
+ (_step) => _step.type === 'cross',
+ )
+ return {
+ srcStep: step.includedSteps[bridgeIndex - 1],
+ bridgeStep: step.includedSteps[bridgeIndex],
+ dstStep: step.includedSteps[bridgeIndex + 1],
+ }
+ }, [step])
+
+ return (
+
+
+
+ {srcStep ? (
+
+ ) : (
+
+ Amount.fromRawAmount(
+ step.action.fromToken.address === zeroAddress
+ ? Native.onChain(step.action.fromToken.chainId)
+ : new Token(step.action.fromToken),
+ step.action.fromAmount,
+ ),
+ [step],
+ )}
+ />
+ )}
+
+ {dstStep ? (
+
+ ) : (
+
+ Amount.fromRawAmount(
+ step.action.toToken.address === zeroAddress
+ ? Native.onChain(step.action.toToken.chainId)
+ : new Token(step.action.toToken),
+ step.estimate.toAmount,
+ ),
+ [step],
+ )}
+ />
+ )}
+
+
+ )
+}
+
+const VerticalDivider: FC<{ className?: string; count: number }> = ({
+ className,
+ count,
+}) => {
+ return (
+
+ {Array.from({ length: count }).map((_, i) => {
+ return i === 0 ? (
+
+ ) : (
+
+ )
+ })}
+
+ )
+}
+
+const SendAction: FC<{
+ label: 'From' | 'To'
+ amount: Amount
+}> = ({ label, amount }) => {
+ const chain = useMemo(
+ () => Chain.fromChainId(amount.currency.chainId)?.name?.toUpperCase(),
+ [amount],
+ )
+
+ return (
+
+
+ {label}{' '}
+ {' '}
+ {chain}
+
+
+ {`${label === 'From' ? 'Send' : 'Receive'} ${amount.toSignificant(6)} ${
+ amount.currency.symbol
+ }`}
+
+
+ )
+}
+
+const SwapAction: FC<{
+ label: 'From' | 'To'
+ action: CrossChainAction
+ estimate: CrossChainEstimate
+}> = ({ label, action, estimate }) => {
+ const { fromAmount, toAmount, chain, isWrap, isUnwrap } = useMemo(() => {
+ const fromToken =
+ action.fromToken.address === zeroAddress
+ ? Native.onChain(action.fromToken.chainId)
+ : new Token(action.fromToken)
+
+ const toToken =
+ action.toToken.address === zeroAddress
+ ? Native.onChain(action.toToken.chainId)
+ : new Token(action.toToken)
+
+ const chain = Chain.fromChainId(
+ label === 'From' ? fromToken.chainId : toToken.chainId,
+ )?.name?.toUpperCase()
+
+ return {
+ fromAmount: Amount.fromRawAmount(fromToken, action.fromAmount),
+ toAmount: Amount.fromRawAmount(toToken, estimate.toAmount),
+ label,
+ chain,
+ isWrap: fromToken.isNative && fromToken.wrapped.equals(toToken),
+ isUnwrap: toToken.isNative && toToken.wrapped.equals(fromToken),
+ }
+ }, [
+ action.fromToken,
+ action.toToken,
+ action.fromAmount,
+ estimate.toAmount,
+ label,
+ ])
+
+ return (
+
+
+ {label}{' '}
+ {' '}
+ {chain}
+
+
+ {isWrap ? (
+ <>Wrap {toAmount.currency.symbol}>
+ ) : isUnwrap ? (
+ <>Unwrap {fromAmount.currency.symbol}>
+ ) : (
+ <>
+ Swap {formatNumber(fromAmount.toExact())}{' '}
+ {fromAmount.currency.symbol}
+ {' -> '}
+ {formatNumber(toAmount.toExact())} {toAmount.currency.symbol}
+ >
+ )}
+
+
+ )
+}
+
+const BridgeAction: FC<{
+ toolDetails: CrossChainToolDetails
+}> = ({ toolDetails }) => {
+ return (
+
+ Via{' '}
+
{' '}
+ {toolDetails.name}
+
+ )
+}
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-switch-tokens-button.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-switch-tokens-button.tsx
index daea657c11..c03a06f5b2 100644
--- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-switch-tokens-button.tsx
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-switch-tokens-button.tsx
@@ -16,7 +16,7 @@ export const CrossChainSwapSwitchTokensButton = () => {
} = useDerivedStateCrossChainSwap()
return (
-
+
{
className="hover:shadow-sm transition-border z-10 group bg-background p-2 border border-accent transition-all rounded-full cursor-pointer"
>
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-token0-input.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-token0-input.tsx
index 4ece36a928..232824af98 100644
--- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-token0-input.tsx
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-token0-input.tsx
@@ -1,15 +1,12 @@
'use client'
-import { useMemo } from 'react'
-import { PREFERRED_CHAINID_ORDER } from 'src/config'
+import { XSWAP_SUPPORTED_CHAIN_IDS, getSortedChainIds } from 'src/config'
import { Web3Input } from 'src/lib/wagmi/components/web3-input'
-import { ChainId } from 'sushi/chain'
-import {
- SUSHIXSWAP_2_SUPPORTED_CHAIN_IDS,
- isWNativeSupported,
-} from 'sushi/config'
+import { isWNativeSupported } from 'sushi/config'
import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider'
+const networks = getSortedChainIds(XSWAP_SUPPORTED_CHAIN_IDS)
+
export const CrossChainSwapToken0Input = () => {
const {
state: { swapAmountString, chainId0, token0 },
@@ -17,21 +14,6 @@ export const CrossChainSwapToken0Input = () => {
isToken0Loading: isLoading,
} = useDerivedStateCrossChainSwap()
- const networks = useMemo(
- () =>
- Array.from(
- new Set([
- ...(PREFERRED_CHAINID_ORDER.filter((el) =>
- SUSHIXSWAP_2_SUPPORTED_CHAIN_IDS.includes(
- el as (typeof SUSHIXSWAP_2_SUPPORTED_CHAIN_IDS)[number],
- ),
- ) as ChainId[]),
- ...SUSHIXSWAP_2_SUPPORTED_CHAIN_IDS,
- ]),
- ),
- [],
- )
-
return (
{
const {
state: { chainId1, token1 },
@@ -21,25 +18,10 @@ export const CrossChainSwapToken1Input = () => {
} = useDerivedStateCrossChainSwap()
const {
- isInitialLoading: isLoading,
+ isLoading,
isFetching,
- data: trade,
- } = useCrossChainSwapTrade()
-
- const networks = useMemo(
- () =>
- Array.from(
- new Set([
- ...(PREFERRED_CHAINID_ORDER.filter((el) =>
- SUSHIXSWAP_2_SUPPORTED_CHAIN_IDS.includes(
- el as (typeof SUSHIXSWAP_2_SUPPORTED_CHAIN_IDS)[number],
- ),
- ) as ChainId[]),
- ...SUSHIXSWAP_2_SUPPORTED_CHAIN_IDS,
- ]),
- ),
- [],
- )
+ data: route,
+ } = useSelectedCrossChainTradeRoute()
return (
{
type="OUTPUT"
disabled
className="border border-accent p-3 bg-white dark:bg-slate-800 rounded-xl"
- value={trade?.amountOut?.toSignificant() ?? ''}
+ value={route?.amountOut?.toSignificant() ?? ''}
chainId={chainId1}
onSelect={setToken1}
currency={token1}
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-button.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-button.tsx
index 32bd0e9eb0..5407a0810c 100644
--- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-button.tsx
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-button.tsx
@@ -5,13 +5,12 @@ import { Button } from '@sushiswap/ui'
import React, { FC, useEffect, useState } from 'react'
import { APPROVE_TAG_XSWAP } from 'src/lib/constants'
import { Checker } from 'src/lib/wagmi/systems/Checker'
-import { SUSHIXSWAP_2_ADDRESS, SushiXSwap2ChainId } from 'sushi/config'
import { ZERO } from 'sushi/math'
import { warningSeverity } from '../../../lib/swap/warningSeverity'
import { CrossChainSwapTradeReviewDialog } from './cross-chain-swap-trade-review-dialog'
import {
- useCrossChainSwapTrade,
useDerivedStateCrossChainSwap,
+ useSelectedCrossChainTradeRoute,
} from './derivedstate-cross-chain-swap-provider'
import { useIsCrossChainSwapMaintenance } from './use-is-cross-chain-swap-maintenance'
@@ -20,15 +19,15 @@ export const CrossChainSwapTradeButton: FC = () => {
const {
state: { swapAmount, swapAmountString, chainId0 },
} = useDerivedStateCrossChainSwap()
- const { data: trade } = useCrossChainSwapTrade()
+ const { data: route, isError } = useSelectedCrossChainTradeRoute()
const [checked, setChecked] = useState(false)
// Reset
useEffect(() => {
- if (warningSeverity(trade?.priceImpact) <= 3) {
+ if (warningSeverity(route?.priceImpact) <= 3) {
setChecked(false)
}
- }, [trade])
+ }, [route])
return (
@@ -44,22 +43,20 @@ export const CrossChainSwapTradeButton: FC = () => {
id="approve-erc20"
fullWidth
amount={swapAmount}
- contract={
- SUSHIXSWAP_2_ADDRESS[chainId0 as SushiXSwap2ChainId]
- }
+ contract={route?.steps?.[0]?.estimate?.approvalAddress}
>
3),
+ warningSeverity(route?.priceImpact) > 3),
)}
color={
- warningSeverity(trade?.priceImpact) >= 3
+ warningSeverity(route?.priceImpact) >= 3
? 'red'
: 'blue'
}
@@ -67,9 +64,9 @@ export const CrossChainSwapTradeButton: FC = () => {
size="xl"
testId="swap"
>
- {!checked && warningSeverity(trade?.priceImpact) >= 3
+ {!checked && warningSeverity(route?.priceImpact) >= 3
? 'Price impact too high'
- : trade?.status === 'NoWay'
+ : isError
? 'No trade found'
: 'Swap'}
@@ -81,7 +78,7 @@ export const CrossChainSwapTradeButton: FC = () => {
- {warningSeverity(trade?.priceImpact) > 3 && (
+ {warningSeverity(route?.priceImpact) > 3 && (
,
- value: BigInt(trade.value ?? 0) as any,
- } as const
- }
-
- if (trade.functionName === SushiXSwapFunctionName.SwapAndBridge) {
- return {
- abi: sushiXSwap2Abi_swapAndBridge,
- functionName: 'swapAndBridge',
- args: trade.writeArgs as NonNullable
,
- value: BigInt(trade.value ?? 0) as any,
- } as const
- }
-}
-
export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
children,
}) => {
+ return (
+
+ <_CrossChainSwapTradeReviewDialog>
+ {children}
+
+
+ )
+}
+
+const _CrossChainSwapTradeReviewDialog: FC<{
+ children: ReactNode
+}> = ({ children }) => {
+ const [showMore, setShowMore] = useState(false)
const [slippagePercent] = useSlippageTolerance()
const { address, chain } = useAccount()
const {
mutate: { setTradeId, setSwapAmount },
state: {
- adapter,
recipient,
swapAmount,
swapAmountString,
@@ -134,8 +119,37 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
} = useDerivedStateCrossChainSwap()
const client0 = usePublicClient({ chainId: chainId0 })
const client1 = usePublicClient({ chainId: chainId1 })
- const { data: trade, isFetching } = useCrossChainSwapTrade()
const { approved } = useApproved(APPROVE_TAG_XSWAP)
+
+ const { open: confirmDialogOpen } = useDialog(DialogType.Confirm)
+ const { open: reviewDialogOpen } = useDialog(DialogType.Review)
+
+ const { data: selectedRoute } = useSelectedCrossChainTradeRoute()
+ const { data: _step, isError: isStepQueryError } = useCrossChainTradeStep({
+ step: selectedRoute?.steps?.[0],
+ query: {
+ enabled: Boolean(
+ approved && address && (confirmDialogOpen || reviewDialogOpen),
+ ),
+ },
+ })
+
+ const step = useMemo(
+ () =>
+ _step ??
+ (selectedRoute?.steps?.[0]
+ ? {
+ ...selectedRoute.steps[0],
+ tokenIn: selectedRoute?.tokenIn,
+ tokenOut: selectedRoute?.tokenOut,
+ amountIn: selectedRoute?.amountIn,
+ amountOut: selectedRoute?.amountOut,
+ amountOutMin: selectedRoute?.amountOutMin,
+ }
+ : undefined),
+ [_step, selectedRoute],
+ )
+
const groupTs = useRef(undefined)
const { refetchChain: refetchBalances } = useRefetchBalances()
@@ -149,39 +163,45 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
dest: StepState.Success,
})
- const tradeRef = useRef(null)
+ const routeRef = useRef(null)
const {
- data: simulation,
- isError,
- error,
- } = useSimulateContract({
- address: SUSHIXSWAP_2_ADDRESS[chainId0 as SushiXSwap2ChainId],
- ...getConfig(trade),
+ data: estGas,
+ isError: isEstGasError,
+ error: estGasError,
+ } = useEstimateGas({
+ chainId: chainId0,
+ to: step?.transactionRequest?.to,
+ data: step?.transactionRequest?.data,
+ value: step?.transactionRequest?.value,
+ account: step?.transactionRequest?.from,
query: {
enabled: Boolean(
- isSushiXSwap2ChainId(chainId0) &&
- isSushiXSwap2ChainId(chainId1) &&
- trade?.writeArgs &&
- trade?.writeArgs.length > 0 &&
+ isXSwapSupportedChainId(chainId0) &&
+ isXSwapSupportedChainId(chainId1) &&
chain?.id === chainId0 &&
- approved &&
- trade?.status !== 'NoWay',
+ approved,
),
},
})
+ const preparedTx = useMemo(() => {
+ return step?.transactionRequest && estGas
+ ? { ...step.transactionRequest, gas: estGas }
+ : undefined
+ }, [step?.transactionRequest, estGas])
+
// onSimulateError
useEffect(() => {
- if (error) {
- console.error('cross chain swap prepare error', error)
- if (error.message.startsWith('user rejected transaction')) return
+ if (estGasError) {
+ console.error('cross chain swap prepare error', estGasError)
+ if (estGasError.message.startsWith('user rejected transaction')) return
sendAnalyticsEvent(SwapEventName.XSWAP_ESTIMATE_GAS_CALL_FAILED, {
- error: error.message,
+ error: estGasError.message,
})
}
- }, [error])
+ }, [estGasError])
const trace = useTrace()
@@ -195,7 +215,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
setSwapAmount('')
- if (!tradeRef?.current || !chainId0) return
+ if (!routeRef?.current || !chainId0) return
sendAnalyticsEvent(SwapEventName.XSWAP_SIGNED, {
...trace,
@@ -211,29 +231,55 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
chainId: chainId0,
txHash: hash,
promise: receiptPromise,
- summary: {
- pending: `Swapping ${tradeRef?.current?.amountIn?.toSignificant(6)} ${
- tradeRef?.current?.amountIn?.currency.symbol
- } to bridge token ${tradeRef?.current?.srcBridgeToken?.symbol}`,
- completed: `Swap ${tradeRef?.current?.amountIn?.toSignificant(6)} ${
- tradeRef?.current?.amountIn?.currency.symbol
- } to bridge token ${tradeRef?.current?.srcBridgeToken?.symbol}`,
- failed: `Something went wrong when trying to swap ${tradeRef?.current?.amountIn?.currency.symbol} to bridge token`,
- },
+ summary:
+ routeRef?.current?.steps?.[0]?.includedSteps?.[0]?.type === 'cross'
+ ? {
+ pending: `Sending ${routeRef?.current?.amountIn?.toSignificant(
+ 6,
+ )} ${routeRef?.current?.amountIn?.currency.symbol} to ${
+ Chain.fromChainId(routeRef?.current?.toChainId)?.name
+ }`,
+ completed: `Sent ${routeRef?.current?.amountIn?.toSignificant(
+ 6,
+ )} ${routeRef?.current?.amountIn?.currency.symbol} to ${
+ Chain.fromChainId(routeRef?.current?.toChainId)?.name
+ }`,
+ failed: `Something went wrong when trying to send ${
+ routeRef?.current?.amountIn?.currency.symbol
+ } to ${Chain.fromChainId(routeRef?.current?.toChainId)?.name}`,
+ }
+ : {
+ pending: `Swapping ${routeRef.current?.amountIn?.toSignificant(
+ 6,
+ )} ${
+ routeRef?.current?.amountIn?.currency.symbol
+ } to bridge token ${
+ routeRef?.current?.steps?.[0]?.includedSteps?.[0]?.action
+ .toToken.symbol
+ }`,
+ completed: `Swapped ${routeRef?.current?.amountIn?.toSignificant(
+ 6,
+ )} ${
+ routeRef?.current?.amountIn?.currency.symbol
+ } to bridge token ${
+ routeRef?.current?.steps?.[0]?.includedSteps?.[0]?.action
+ .toToken.symbol
+ }`,
+ failed: `Something went wrong when trying to swap ${routeRef?.current?.amountIn?.currency.symbol} to bridge token`,
+ },
timestamp: groupTs.current,
groupTimestamp: groupTs.current,
})
try {
const receipt = await receiptPromise
- const trade = tradeRef.current
+ const trade = routeRef.current
if (receipt.status === 'success') {
sendAnalyticsEvent(SwapEventName.XSWAP_SRC_TRANSACTION_COMPLETED, {
txHash: hash,
address: receipt.from,
src_chain_id: trade?.amountIn?.currency?.chainId,
dst_chain_id: trade?.amountOut?.currency?.chainId,
- transaction_type: trade?.transactionType,
})
} else {
sendAnalyticsEvent(SwapEventName.XSWAP_SRC_TRANSACTION_FAILED, {
@@ -241,7 +287,6 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
address: receipt.from,
src_chain_id: trade?.amountIn?.currency?.chainId,
dst_chain_id: trade?.amountOut?.currency?.chainId,
- transaction_type: trade?.transactionType,
})
setStepStates({
@@ -297,27 +342,24 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
}, [])
const {
- writeContractAsync,
+ sendTransactionAsync,
isPending: isWritePending,
data: hash,
reset,
- } = useWriteContract({
+ } = useSendTransaction({
mutation: {
onSuccess: onWriteSuccess,
onError: onWriteError,
onMutate: () => {
- if (tradeRef && trade) {
- tradeRef.current = trade
+ if (routeRef && selectedRoute) {
+ routeRef.current = selectedRoute
}
},
},
})
- // Speeds up typechecking in the useMemo below
- const _simulation: { request: any } | undefined = simulation
-
const write = useMemo(() => {
- if (!_simulation?.request) return undefined
+ if (!preparedTx) return undefined
return async (confirm: () => void) => {
setStepStates({
@@ -328,119 +370,67 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
confirm()
try {
- await writeContractAsync(_simulation.request)
+ await sendTransactionAsync(preparedTx)
} catch {}
}
- }, [writeContractAsync, _simulation?.request])
-
- const { data: lzData } = useLayerZeroScanLink({
- tradeId,
- network1: chainId1,
- network0: chainId0,
- txHash: hash,
- enabled: adapter === SushiXSwap2Adapter.Stargate,
- })
+ }, [sendTransactionAsync, preparedTx])
- const { data: axelarScanData } = useAxelarScanLink({
- tradeId,
- network1: chainId1,
- network0: chainId0,
+ const { data: lifiData } = useLiFiStatus({
+ tradeId: tradeId,
txHash: hash,
- enabled: adapter === SushiXSwap2Adapter.Squid,
})
const { data: receipt } = useTransaction({
chainId: chainId1,
- hash: (adapter === SushiXSwap2Adapter.Stargate
- ? lzData?.dstTxHash
- : axelarScanData?.dstTxHash) as `0x${string}` | undefined,
+ hash: lifiData?.receiving?.txHash,
query: {
- enabled: Boolean(
- adapter === SushiXSwap2Adapter.Stargate
- ? lzData?.dstTxHash
- : axelarScanData?.dstTxHash,
- ),
+ enabled: Boolean(lifiData?.receiving?.txHash),
},
})
useEffect(() => {
- if (lzData?.status === 'DELIVERED') {
- setStepStates({
- source: StepState.Success,
- bridge: StepState.Success,
- dest: StepState.Success,
- })
- }
- if (lzData?.status === 'FAILED') {
- setStepStates((prev) => ({
- ...prev,
- dest: StepState.PartialSuccess,
- }))
- }
- }, [lzData?.status])
-
- useEffect(() => {
- if (axelarScanData?.status === 'success') {
- setStepStates({
- source: StepState.Success,
- bridge: StepState.Success,
- dest: StepState.Success,
- })
- }
- if (axelarScanData?.status === 'partial_success') {
- setStepStates((prev) => ({
- ...prev,
- bridge: StepState.Success,
- dest: StepState.PartialSuccess,
- }))
- }
- }, [axelarScanData?.status])
-
- // biome-ignore lint/correctness/useExhaustiveDependencies:
- useEffect(() => {
- if (
- axelarScanData?.link &&
- groupTs.current &&
- stepStates.source === StepState.Success
- ) {
- void createInfoToast({
- account: address,
- type: 'squid',
- chainId: chainId0,
- href: axelarScanData.link,
- summary: `Bridging ${tradeRef?.current?.srcBridgeToken?.symbol} from ${
- Chain.from(chainId0)?.name
- } to ${Chain.from(chainId1)?.name}`,
- timestamp: new Date().getTime(),
- groupTimestamp: groupTs.current,
- })
+ if (lifiData?.status === 'DONE') {
+ if (lifiData?.substatus === 'COMPLETED') {
+ setStepStates({
+ source: StepState.Success,
+ bridge: StepState.Success,
+ dest: StepState.Success,
+ })
+ }
+ if (lifiData?.substatus === 'PARTIAL') {
+ setStepStates({
+ source: StepState.Success,
+ bridge: StepState.Success,
+ dest: StepState.PartialSuccess,
+ })
+ }
}
- }, [axelarScanData?.link])
+ }, [lifiData])
// biome-ignore lint/correctness/useExhaustiveDependencies:
useEffect(() => {
if (
- lzData?.link &&
+ lifiData?.lifiExplorerLink &&
groupTs.current &&
stepStates.source === StepState.Success
) {
void createInfoToast({
account: address,
- type: 'stargate',
+ type: 'xswap',
chainId: chainId0,
- href: lzData.link,
- summary: `Bridging ${tradeRef?.current?.srcBridgeToken?.symbol} from ${
+ href: lifiData.lifiExplorerLink,
+ summary: `Bridging ${routeRef?.current?.fromToken?.symbol} from ${
Chain.from(chainId0)?.name
} to ${Chain.from(chainId1)?.name}`,
timestamp: new Date().getTime(),
groupTimestamp: groupTs.current,
})
}
- }, [lzData?.link])
+ }, [lifiData?.lifiExplorerLink])
// biome-ignore lint/correctness/useExhaustiveDependencies:
useEffect(() => {
- if (receipt && groupTs.current) {
+ if (receipt?.hash && groupTs.current) {
void createToast({
account: address,
type: 'swap',
@@ -461,36 +451,125 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
.then(() => {
sendAnalyticsEvent(SwapEventName.XSWAP_DST_TRANSACTION_COMPLETED, {
chain_id: chainId1,
- txHash: axelarScanData?.dstTxHash,
+ txHash: lifiData?.receiving?.txHash,
})
refetchBalances(chainId1)
})
.then(reset),
- summary: {
- pending: `Swapping ${
- tradeRef?.current?.dstBridgeToken?.symbol
- } to ${tradeRef?.current?.amountOut?.toSignificant(6)} ${
- tradeRef?.current?.amountOut?.currency.symbol
- }`,
- completed: `Swap ${
- tradeRef?.current?.dstBridgeToken?.symbol
- } to ${tradeRef?.current?.amountOut?.toSignificant(6)} ${
- tradeRef?.current?.amountOut?.currency.symbol
- }`,
- failed: `Something went wrong when trying to swap ${
- tradeRef?.current?.dstBridgeToken?.symbol
- } to ${tradeRef?.current?.amountOut?.toSignificant(6)} ${
- tradeRef?.current?.amountOut?.currency.symbol
- }`,
- },
+ summary:
+ routeRef?.current?.steps?.[0]?.includedSteps?.[1]?.type === 'swap' ||
+ routeRef?.current?.steps?.[0]?.includedSteps?.[2]?.type === 'swap'
+ ? {
+ pending: `Swapping ${
+ routeRef?.current?.steps?.[0]?.includedSteps[2]?.action
+ .fromToken?.symbol
+ } to ${routeRef?.current?.amountOut?.toSignificant(6)} ${
+ routeRef?.current?.amountOut?.currency.symbol
+ }`,
+ completed: `Swapped ${
+ routeRef?.current?.steps?.[0]?.includedSteps[2]?.action
+ .fromToken?.symbol
+ } to ${routeRef?.current?.amountOut?.toSignificant(6)} ${
+ routeRef?.current?.amountOut?.currency.symbol
+ }`,
+ failed: `Something went wrong when trying to swap ${
+ routeRef?.current?.steps?.[0]?.includedSteps[2]?.action
+ .fromToken?.symbol
+ } to ${routeRef?.current?.amountOut?.toSignificant(6)} ${
+ routeRef?.current?.amountOut?.currency.symbol
+ }`,
+ }
+ : {
+ pending: `Receiving ${routeRef?.current?.amountOut?.toSignificant(
+ 6,
+ )} ${routeRef?.current?.amountOut?.currency.symbol} on ${
+ Chain.fromChainId(routeRef?.current?.toChainId!)?.name
+ }`,
+ completed: `Received ${routeRef?.current?.amountOut?.toSignificant(
+ 6,
+ )} ${routeRef?.current?.amountOut?.currency.symbol} on ${
+ Chain.fromChainId(routeRef?.current?.toChainId!)?.name
+ }`,
+ failed: `Something went wrong when trying to receive ${routeRef?.current?.amountOut?.toSignificant(
+ 6,
+ )} ${routeRef?.current?.amountOut?.currency.symbol} on ${
+ Chain.fromChainId(routeRef?.current?.toChainId!)?.name
+ }`,
+ },
timestamp: new Date().getTime(),
groupTimestamp: groupTs.current,
})
}
- }, [receipt])
+ }, [receipt?.hash])
+
+ const { executionDuration, feesBreakdown, totalFeesUSD, chainId0Fees } =
+ useMemo(() => {
+ if (!step)
+ return {
+ executionDuration: undefined,
+ feesBreakdown: undefined,
+ gasFeesUSD: undefined,
+ protocolFeesUSD: undefined,
+ totalFeesUSD: undefined,
+ }
+
+ const executionDurationSeconds = step.estimate.executionDuration
+ const executionDurationMinutes = Math.floor(executionDurationSeconds / 60)
+
+ const executionDuration =
+ executionDurationSeconds < 60
+ ? `${executionDurationSeconds} seconds`
+ : `${executionDurationMinutes} minutes`
+
+ const { feesBreakdown, totalFeesUSD } = getCrossChainFeesBreakdown([step])
+
+ const chainId0Fees = (
+ feesBreakdown.gas.get(step.tokenIn.chainId)?.amount ??
+ Amount.fromRawAmount(Native.onChain(step.tokenIn.chainId), 0)
+ )
+ .add(
+ feesBreakdown.protocol.get(step.tokenIn.chainId)?.amount ??
+ Amount.fromRawAmount(Native.onChain(step.tokenIn.chainId), 0),
+ )
+ .toExact()
+
+ return {
+ executionDuration,
+ feesBreakdown,
+ totalFeesUSD,
+ chainId0Fees,
+ }
+ }, [step])
+
+ const { data: price } = usePrice({
+ chainId: token1?.chainId,
+ address: token1?.wrapped.address,
+ })
+
+ const amountOutUSD = useMemo(
+ () =>
+ price && step?.amountOut
+ ? `${(
+ (price * Number(step.amountOut.quotient)) /
+ 10 ** step.amountOut.currency.decimals
+ ).toFixed(2)}`
+ : undefined,
+ [step?.amountOut, price],
+ )
+
+ const amountOutMinUSD = useMemo(
+ () =>
+ price && step?.amountOutMin
+ ? `${(
+ (price * Number(step.amountOutMin.quotient)) /
+ 10 ** step.amountOutMin.currency.decimals
+ ).toFixed(2)}`
+ : undefined,
+ [step?.amountOutMin, price],
+ )
return (
-
+ <>
{({ confirm }) => (
<>
@@ -498,7 +577,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
0 &&
- stringify(error).includes('insufficient funds'),
+ stringify(estGasError).includes('insufficient funds'),
)}
>
@@ -507,7 +586,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
{Chain.fromChainId(chainId0)?.name} to cover the network
fee. Please lower your input amount or{' '}
swap for more {Native.onChain(chainId0).symbol}
@@ -521,10 +600,10 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
- {isFetching ? (
+ {!step?.amountOut ? (
) : (
- `Receive ${trade?.amountOut?.toSignificant(6)} ${
+ `Receive ${step?.amountOut?.toSignificant(6)} ${
token1?.symbol
}`
)}
@@ -536,65 +615,282 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
-
-
- {chainName?.[chainId0]
- ?.replace('Mainnet Shard 0', '')
- ?.replace('Mainnet', '')
- ?.trim()}
-
-
- to
- {' '}
- {chainName?.[chainId1]
- ?.replace('Mainnet Shard 0', '')
- ?.replace('Mainnet', '')
- ?.trim()}
-
-
-
- {isFetching || !trade?.priceImpact ? (
+
+ {!executionDuration ? (
) : (
- `${
- trade?.priceImpact?.lessThan(ZERO)
- ? '+'
- : trade?.priceImpact?.greaterThan(ZERO)
- ? '-'
- : ''
- }${Math.abs(Number(trade?.priceImpact?.toFixed(2)))}%`
+ `${executionDuration}`
)}
-
- {isFetching || !trade?.amountOutMin ? (
-
- ) : (
- `${trade?.amountOutMin?.toSignificant(6)} ${
- token1?.symbol
- }`
- )}
-
-
-
-
-
-
+ {showMore ? (
+ <>
+
+ {!step?.priceImpact ? (
+
+
+
+ ) : (
+ `${
+ step.priceImpact.lessThan(ZERO)
+ ? '+'
+ : step.priceImpact.greaterThan(ZERO)
+ ? '-'
+ : ''
+ }${Math.abs(Number(step.priceImpact.toFixed(2)))}%`
+ )}
+
+
+ {feesBreakdown && feesBreakdown.gas.size > 0 ? (
+
+
+ {feesBreakdown.gas.get(chainId0) ? (
+
+ {formatNumber(
+ feesBreakdown.gas
+ .get(chainId0)!
+ .amount.toExact(),
+ )}{' '}
+ {
+ feesBreakdown.gas.get(chainId0)!.amount
+ .currency.symbol
+ }{' '}
+
+ (
+ {formatUSD(
+ feesBreakdown.gas.get(chainId0)!
+ .amountUSD,
+ )}
+ )
+
+
+ ) : null}
+ {feesBreakdown.gas.get(chainId1) ? (
+
+ {formatNumber(
+ feesBreakdown.gas
+ .get(chainId1)!
+ .amount.toExact(),
+ )}{' '}
+ {
+ feesBreakdown.gas.get(chainId1)!.amount
+ .currency.symbol
+ }{' '}
+
+ (
+ {formatUSD(
+ feesBreakdown.gas.get(chainId1)!
+ .amountUSD,
+ )}
+
+ )
+
+ ) : null}
+
+
+ ) : null}
+ {feesBreakdown && feesBreakdown.protocol.size > 0 ? (
+
+
+ {feesBreakdown.protocol.get(chainId0) ? (
+
+ {formatNumber(
+ feesBreakdown.protocol
+ .get(chainId0)!
+ .amount.toExact(),
+ )}{' '}
+ {
+ feesBreakdown.protocol.get(chainId0)!.amount
+ .currency.symbol
+ }{' '}
+
+ (
+ {formatUSD(
+ feesBreakdown.protocol.get(chainId0)!
+ .amountUSD,
+ )}
+ )
+
+
+ ) : null}
+ {feesBreakdown.protocol.get(chainId1) ? (
+
+ {formatNumber(
+ feesBreakdown.protocol
+ .get(chainId1)!
+ .amount.toExact(),
+ )}{' '}
+ {
+ feesBreakdown.protocol.get(chainId1)!.amount
+ .currency.symbol
+ }{' '}
+
+ (
+ {formatUSD(
+ feesBreakdown.protocol.get(chainId1)!
+ .amountUSD,
+ )}
+ )
+
+
+ ) : null}
+
+
+ ) : null}
+
+
+ {!step?.amountOut ? (
+
+ ) : (
+ {`${step.amountOut.toSignificant(
+ 6,
+ )} ${token1?.symbol}`}
+ )}
+ {!amountOutUSD ? (
+
+ ) : (
+
+ {formatUSD(amountOutUSD)}
+
+ )}
+
+
+
+
+ {!step?.amountOutMin ? (
+
+ ) : (
+ {`${step.amountOutMin.toSignificant(
+ 6,
+ )} ${token1?.symbol}`}
+ )}
+ {!amountOutMinUSD ? (
+
+ ) : (
+
+ {formatUSD(amountOutMinUSD)}
+
+ )}
+
+
+ >
+ ) : (
+ <>
+
+ {!totalFeesUSD ? (
+
+ ) : (
+
+
+ {formatNumber(chainId0Fees)}{' '}
+ {
+ feesBreakdown.gas.get(chainId0)!.amount
+ .currency.symbol
+ }{' '}
+
+ ({formatUSD(totalFeesUSD)})
+
+
+
+ )}
+
+
+
+ {!step?.amountOut ? (
+
+ ) : (
+ {`${step.amountOut.toSignificant(
+ 6,
+ )} ${token1?.symbol}`}
+ )}
+ {!amountOutUSD ? (
+
+ ) : (
+
+ {formatUSD(amountOutUSD)}
+
+ )}
+
+
+ >
+ )}
+
+
+ setShowMore(!showMore)}
+ variant="ghost"
+ >
+ {showMore ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+ >
+ )}
+
+
+ {step && (
+
+
+
+
+
+ )}
{recipient && (
@@ -624,23 +920,24 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
write?.(confirm)}
disabled={
isWritePending ||
Boolean(!write && +swapAmountString > 0) ||
- isError
+ isEstGasError ||
+ isStepQueryError
}
color={
- isError
+ isEstGasError || isStepQueryError
? 'red'
- : warningSeverity(trade?.priceImpact) >= 3
+ : warningSeverity(step?.priceImpact) >= 3
? 'red'
: 'blue'
}
testId="confirm-swap"
>
- {isError ? (
+ {isEstGasError || isStepQueryError ? (
'Shoot! Something went wrong :('
) : isWritePending ? (
Confirm Swap
@@ -662,11 +959,10 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
@@ -695,22 +991,6 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({
-
+ >
)
}
-
-const getBridgeUrl = (
- adapter: SushiXSwap2Adapter | undefined,
- lzData: Awaited>['data'],
- axelarScanData: Awaited>['data'],
-) =>
- adapter === SushiXSwap2Adapter.Stargate ? lzData?.link : axelarScanData?.link
-
-const getDstTxHash = (
- adapter: SushiXSwap2Adapter | undefined,
- lzData: Awaited>['data'],
- axelarScanData: Awaited>['data'],
-) =>
- adapter === SushiXSwap2Adapter.Stargate
- ? lzData?.dstTxHash
- : axelarScanData?.dstTxHash
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-route.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-route.tsx
index 5b4a79bdc0..9a86a1a0de 100644
--- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-route.tsx
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-route.tsx
@@ -1,90 +1,78 @@
'use client'
-import { Currency, SkeletonText } from '@sushiswap/ui'
-import { SquidIcon } from '@sushiswap/ui/icons/SquidIcon'
-import { SushiXSwap2Adapter } from 'src/lib/swap/cross-chain'
+import { classNames } from '@sushiswap/ui'
+import React from 'react'
import { Chain } from 'sushi/chain'
-import { STARGATE_TOKEN } from 'sushi/config'
import {
- useCrossChainSwapTrade,
useDerivedStateCrossChainSwap,
+ useSelectedCrossChainTradeRoute,
} from './derivedstate-cross-chain-swap-provider'
export const CrossChainSwapTradeReviewRoute = () => {
const {
- state: { adapter, chainId0, token0, token1, chainId1 },
+ state: { chainId0 },
} = useDerivedStateCrossChainSwap()
- const { data: trade, isInitialLoading: isTradeLoading } =
- useCrossChainSwapTrade()
+ const { data: trade } = useSelectedCrossChainTradeRoute()
return (
-
-
-
- From: {Chain.fromChainId(chainId0)?.name?.toUpperCase()}
-
-
- {isTradeLoading || !trade?.srcBridgeToken || !token0 ? (
-
- ) : (
-
- Swap {token0.symbol} to {trade.srcBridgeToken.symbol}
-
- )}
-
-
-
-
-
-
-
-
-
- {isTradeLoading || !trade?.srcBridgeToken ? (
-
- ) : (
-
- Bridge {trade.srcBridgeToken.symbol}
-
- )}
-
-
-
-
-
-
-
-
- To: {Chain.fromChainId(chainId1)?.name?.toUpperCase()}
-
-
- {isTradeLoading || !trade?.dstBridgeToken || !token1 ? (
-
- ) : (
-
- Swap {trade.dstBridgeToken.symbol} to {token1.symbol}
-
- )}
-
-
-
- )
-}
+
+ {trade?.steps?.[0]?.type === 'lifi' &&
+ trade.steps[0].includedSteps.map((step, i) => {
+ return (
+
+ {i > 0 ? (
+
+
+
+
+
+ ) : null}
+ {step.type === 'swap' ? (
+
+
+ {step.action.fromChainId === chainId0 ? 'From' : 'To'}:{' '}
+ {Chain.fromChainId(
+ step.action.fromChainId,
+ )?.name?.toUpperCase()}
+
-const CrossChainAdapter = ({
- adapter,
-}: { adapter: SushiXSwap2Adapter | undefined }) => {
- return adapter === SushiXSwap2Adapter.Stargate ? (
-
- Via
-
- Stargate
-
- ) : (
-
- Via
-
- Squid
-
+
+
+ Swap {step.action.fromToken.symbol} to{' '}
+ {step.action.toToken.symbol}
+
+
+
+ ) : step.type === 'cross' ? (
+
+
+ Via{' '}
+
{' '}
+
+ {step.toolDetails.name}
+
+
+
+ Bridge {step.action.fromToken.symbol}
+
+
+ ) : null}
+
+ )
+ })}
+
)
}
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-stats.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-stats.tsx
index 1241e74ed2..a72c66da30 100644
--- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-stats.tsx
+++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-stats.tsx
@@ -1,31 +1,23 @@
'use client'
-import {
- Tooltip,
- TooltipPrimitive,
- TooltipProvider,
- TooltipTrigger,
- classNames,
-} from '@sushiswap/ui'
-import { Collapsible } from '@sushiswap/ui'
-import { Explainer } from '@sushiswap/ui'
-import { SkeletonBox } from '@sushiswap/ui'
-import React, { FC } from 'react'
+import { Collapsible, Explainer, SkeletonBox, classNames } from '@sushiswap/ui'
+import React, { FC, useMemo } from 'react'
import { Chain } from 'sushi/chain'
-import { Native } from 'sushi/currency'
-import { shortenAddress } from 'sushi/format'
+import { formatUSD, shortenAddress } from 'sushi/format'
import { ZERO } from 'sushi/math'
import { isAddress } from 'viem'
+import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain'
import { AddressToEnsResolver } from 'src/lib/wagmi/components/account/AddressToEnsResolver'
import { useAccount } from 'wagmi'
import {
warningSeverity,
warningSeverityClassName,
} from '../../../lib/swap/warningSeverity'
+import { CrossChainSwapFeesHoverCard } from './cross-chain-swap-fees-hover-card'
import {
- useCrossChainSwapTrade,
useDerivedStateCrossChainSwap,
+ useSelectedCrossChainTradeRoute,
} from './derivedstate-cross-chain-swap-provider'
export const CrossChainSwapTradeStats: FC = () => {
@@ -33,11 +25,15 @@ export const CrossChainSwapTradeStats: FC = () => {
const {
state: { chainId0, chainId1, swapAmountString, recipient },
} = useDerivedStateCrossChainSwap()
- const { isInitialLoading: isLoading, data: trade } = useCrossChainSwapTrade()
- const loading = Boolean(isLoading && !trade?.writeArgs)
+ const { isLoading, data: trade, isError } = useSelectedCrossChainTradeRoute()
+
+ const feeData = useMemo(
+ () => (trade?.steps ? getCrossChainFeesBreakdown(trade.steps) : undefined),
+ [trade?.steps],
+ )
return (
- 0 && trade?.status !== 'NoWay'}>
+ 0 && !isError}>
@@ -49,7 +45,7 @@ export const CrossChainSwapTradeStats: FC = () => {
'text-sm font-semibold text-gray-700 text-right dark:text-slate-400',
)}
>
- {loading || !trade?.priceImpact ? (
+ {isLoading || !trade?.priceImpact ? (
) : trade?.priceImpact ? (
`${
@@ -68,7 +64,7 @@ export const CrossChainSwapTradeStats: FC = () => {
Est. received
- {loading || !trade?.amountOut ? (
+ {isLoading || !trade?.amountOut ? (
) : (
`${trade?.amountOut?.toSignificant(6) ?? '0.00'} ${
@@ -83,7 +79,7 @@ export const CrossChainSwapTradeStats: FC = () => {
Min. received
- {loading || !trade?.amountOutMin ? (
+ {isLoading || !trade?.amountOutMin ? (
) : (
`${trade?.amountOutMin?.toSignificant(6) ?? '0.00'} ${
@@ -98,44 +94,21 @@ export const CrossChainSwapTradeStats: FC = () => {
Network fee
- {loading || !trade?.gasSpent || trade.gasSpent === '0' ? (
+ {isLoading || !feeData ? (
- ) : trade?.gasSpent ? (
-
-
-
-
- {trade.gasSpent} {Native.onChain(chainId0).symbol}
-
-
-
-
-
-
Network Fee
-
- {trade.gasSpent} {Native.onChain(chainId0).symbol}
-
-
-
-
on Origin Chain
-
- {trade.srcGasFee} {Native.onChain(chainId0).symbol}
-
-
-
-
on Dest. Chain
-
- {trade.bridgeFee} {Native.onChain(chainId0).symbol}
-
-
-
-
-
-
- ) : null}
+ ) : (
+
+
+ {formatUSD(feeData.totalFeesUSD)}
+
+
+ )}
diff --git a/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx b/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx
index a7b1609163..95435987a3 100644
--- a/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx
+++ b/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx
@@ -9,27 +9,23 @@ import {
createContext,
useCallback,
useContext,
+ useEffect,
useMemo,
useState,
} from 'react'
-import { useCrossChainTrade } from 'src/lib/hooks'
+import { isXSwapSupportedChainId } from 'src/config'
+import { useCrossChainTradeRoutes as _useCrossChainTradeRoutes } from 'src/lib/hooks/react-query'
import { useSlippageTolerance } from 'src/lib/hooks/useSlippageTolerance'
import { replaceNetworkSlug } from 'src/lib/network'
-import { SushiXSwap2Adapter } from 'src/lib/swap/cross-chain'
+import { CrossChainRoute, CrossChainRouteOrder } from 'src/lib/swap/cross-chain'
import { useTokenWithCache } from 'src/lib/wagmi/hooks/tokens/useTokenWithCache'
import { ChainId, ChainKey } from 'sushi/chain'
-import {
- SushiXSwap2ChainId,
- defaultCurrency,
- isSquidAdapterChainId,
- isStargateAdapterChainId,
- isSushiXSwap2ChainId,
-} from 'sushi/config'
+import { defaultCurrency } from 'sushi/config'
import { defaultQuoteCurrency } from 'sushi/config'
-import { Amount, Native, Type, tryParseAmount } from 'sushi/currency'
-import { ZERO } from 'sushi/math'
-import { Address, isAddress } from 'viem'
-import { useAccount, useGasPrice } from 'wagmi'
+import { Amount, Native, Token, Type, tryParseAmount } from 'sushi/currency'
+import { Percent } from 'sushi/math'
+import { Address, isAddress, zeroAddress } from 'viem'
+import { useAccount } from 'wagmi'
const getTokenAsString = (token: Type | string) =>
typeof token === 'string'
@@ -51,10 +47,11 @@ interface State {
setToken0(token0: Type | string): void
setToken1(token1: Type | string): void
setTokens(token0: Type | string, token1: Type | string): void
-
setSwapAmount(swapAmount: string): void
switchTokens(): void
setTradeId: Dispatch
>
+ setSelectedBridge(bridge: string | undefined): void
+ setRouteOrder(order: CrossChainRouteOrder): void
}
state: {
tradeId: string
@@ -64,8 +61,9 @@ interface State {
chainId1: ChainId
swapAmountString: string
swapAmount: Amount | undefined
- recipient: string | undefined
- adapter: SushiXSwap2Adapter | undefined
+ recipient: Address | undefined
+ selectedBridge: string | undefined
+ routeOrder: CrossChainRouteOrder
}
isLoading: boolean
isToken0Loading: boolean
@@ -89,15 +87,16 @@ const DerivedstateCrossChainSwapProvider: FC<
DerivedStateCrossChainSwapProviderProps
> = ({ children, defaultChainId }) => {
const { push } = useRouter()
- const { address } = useAccount()
const pathname = usePathname()
const searchParams = useSearchParams()
const [tradeId, setTradeId] = useState(nanoid())
const [chainId, setChainId] = useState(defaultChainId)
+ const [selectedBridge, setSelectedBridge] = useState(
+ undefined,
+ )
+ const [routeOrder, setRouteOrder] = useState('CHEAPEST')
- const chainId0 = isSushiXSwap2ChainId(chainId as ChainId)
- ? (chainId as SushiXSwap2ChainId)
- : ChainId.ETHEREUM
+ const chainId0 = isXSwapSupportedChainId(chainId) ? chainId : ChainId.ETHEREUM
// Get the searchParams and complete with defaults.
// This handles the case where some params might not be provided by the user
@@ -208,8 +207,8 @@ const DerivedstateCrossChainSwapProvider: FC<
)
// Update the URL with a new token0
- const setToken0 = useCallback<{ (_token0: string | Type): void }>(
- (_token0) => {
+ const setToken0 = useCallback(
+ (_token0: string | Type) => {
// If entity is provided, parse it to a string
const token0 = getTokenAsString(_token0)
push(
@@ -221,8 +220,8 @@ const DerivedstateCrossChainSwapProvider: FC<
)
// Update the URL with a new token1
- const setToken1 = useCallback<{ (_token1: string | Type): void }>(
- (_token1) => {
+ const setToken1 = useCallback(
+ (_token1: string | Type) => {
// If entity is provided, parse it to a string
const token1 = getTokenAsString(_token1)
push(
@@ -234,10 +233,8 @@ const DerivedstateCrossChainSwapProvider: FC<
)
// Update the URL with both tokens
- const setTokens = useCallback<{
- (_token0: string | Type, _token1: string | Type): void
- }>(
- (_token0, _token1) => {
+ const setTokens = useCallback(
+ (_token0: string | Type, _token1: string | Type) => {
// If entity is provided, parse it to a string
const token0 = getTokenAsString(_token0)
const token1 = getTokenAsString(_token1)
@@ -254,8 +251,8 @@ const DerivedstateCrossChainSwapProvider: FC<
)
// Update the URL with a new swapAmount
- const setSwapAmount = useCallback<{ (swapAmount: string): void }>(
- (swapAmount) => {
+ const setSwapAmount = useCallback(
+ (swapAmount: string) => {
push(
`${pathname}?${createQueryString([
{ name: 'swapAmount', value: swapAmount },
@@ -289,26 +286,33 @@ const DerivedstateCrossChainSwapProvider: FC<
keepPreviousData: false,
})
- const adapter =
- isStargateAdapterChainId(chainId0) && isStargateAdapterChainId(chainId1)
- ? SushiXSwap2Adapter.Stargate
- : isSquidAdapterChainId(chainId0) && isSquidAdapterChainId(chainId1)
- ? SushiXSwap2Adapter.Squid
- : undefined
+ const swapAmountString = defaultedParams.get('swapAmount') || ''
+
+ const [_token0, _token1] = useMemo(
+ () => [
+ defaultedParams.get('token0') === 'NATIVE'
+ ? Native.onChain(chainId0)
+ : token0,
+ defaultedParams.get('token1') === 'NATIVE'
+ ? Native.onChain(chainId1)
+ : token1,
+ ],
+ [defaultedParams, chainId0, chainId1, token0, token1],
+ )
+
+ const swapAmount = useMemo(
+ () => tryParseAmount(swapAmountString, _token0),
+ [_token0, swapAmountString],
+ )
+
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
+ useEffect(() => {
+ setSelectedBridge(undefined)
+ }, [swapAmount, routeOrder])
return (
{
- const swapAmountString = defaultedParams.get('swapAmount') || ''
- const _token0 =
- defaultedParams.get('token0') === 'NATIVE'
- ? Native.onChain(chainId0)
- : token0
- const _token1 =
- defaultedParams.get('token1') === 'NATIVE'
- ? Native.onChain(chainId1)
- : token1
-
return {
mutate: {
setChainId0,
@@ -319,27 +323,28 @@ const DerivedstateCrossChainSwapProvider: FC<
setTradeId,
switchTokens,
setSwapAmount,
+ setSelectedBridge,
+ setRouteOrder,
},
state: {
tradeId,
- recipient: address ?? '',
+ recipient: undefined,
chainId0,
chainId1,
swapAmountString,
- swapAmount: tryParseAmount(swapAmountString, _token0),
+ swapAmount,
token0: _token0,
token1: _token1,
- adapter,
+ selectedBridge,
+ routeOrder,
},
isLoading: token0Loading || token1Loading,
isToken0Loading: token0Loading,
isToken1Loading: token1Loading,
}
}, [
- address,
chainId0,
chainId1,
- defaultedParams,
setChainId0,
setChainId1,
setSwapAmount,
@@ -347,12 +352,15 @@ const DerivedstateCrossChainSwapProvider: FC<
setToken1,
setTokens,
switchTokens,
- token0,
+ swapAmount,
+ swapAmountString,
+ _token0,
token0Loading,
- token1,
+ _token1,
token1Loading,
tradeId,
- adapter,
+ selectedBridge,
+ routeOrder,
])}
>
{children}
@@ -371,78 +379,124 @@ const useDerivedStateCrossChainSwap = () => {
return context
}
-const useCrossChainSwapTrade = () => {
+const useCrossChainTradeRoutes = () => {
const {
- state: {
- token0,
- chainId0,
- chainId1,
- swapAmount,
- token1,
- recipient,
- adapter,
- },
+ state: { token1, swapAmount, selectedBridge, routeOrder },
+ mutate: { setSelectedBridge },
} = useDerivedStateCrossChainSwap()
- const [slippagePercent] = useSlippageTolerance()
+ const [slippagePercent] = useSlippageTolerance()
const { address } = useAccount()
- const { data: srcGasPrice } = useGasPrice({
- chainId: chainId0,
- query: { enabled: swapAmount?.greaterThan(ZERO) },
- })
- const { data: dstGasPrice } = useGasPrice({
- chainId: chainId1,
- query: { enabled: swapAmount?.greaterThan(ZERO) },
+ const query = _useCrossChainTradeRoutes({
+ fromAmount: swapAmount,
+ toToken: token1,
+ slippage: slippagePercent,
+ fromAddress: address,
+ order: routeOrder,
})
- const stargateCrossChainTrade = useCrossChainTrade({
- adapter: SushiXSwap2Adapter.Stargate,
- srcChainId: chainId0 as SushiXSwap2ChainId,
- dstChainId: chainId1 as SushiXSwap2ChainId,
- srcGasPrice: srcGasPrice,
- dstGasPrice: dstGasPrice,
- tokenIn: token0,
- tokenOut: token1,
- amount: swapAmount,
- slippagePercentage: slippagePercent.toFixed(2),
- from: address,
- recipient: recipient as Address,
- query: {
- refetchInterval: 15000,
- enabled:
- adapter === SushiXSwap2Adapter.Stargate &&
- swapAmount?.greaterThan(ZERO),
- },
- })
+ useEffect(() => {
+ if (
+ query.data?.length &&
+ (typeof selectedBridge === 'undefined' ||
+ !query.data.find((route) => route.steps[0].tool === selectedBridge))
+ ) {
+ setSelectedBridge(query.data[0].steps[0].tool)
+ }
+ }, [query.data, selectedBridge, setSelectedBridge])
+
+ return query
+}
- const squidCrossChainTrade = useCrossChainTrade({
- adapter: SushiXSwap2Adapter.Squid,
- srcChainId: chainId0 as SushiXSwap2ChainId,
- dstChainId: chainId1 as SushiXSwap2ChainId,
- srcGasPrice: srcGasPrice,
- dstGasPrice: dstGasPrice,
- tokenIn: token0,
- tokenOut: token1,
- amount: swapAmount,
- slippagePercentage: slippagePercent.toFixed(2),
- from: address,
- recipient: recipient as Address,
- query: {
- refetchInterval: 15000,
- enabled:
- adapter !== SushiXSwap2Adapter.Stargate &&
- swapAmount?.greaterThan(ZERO),
- },
- })
+export interface UseSelectedCrossChainTradeRouteReturn extends CrossChainRoute {
+ tokenIn: Type
+ tokenOut: Type
+ amountIn?: Amount
+ amountOut?: Amount
+ amountOutMin?: Amount
+ priceImpact?: Percent
+}
+
+const useSelectedCrossChainTradeRoute = () => {
+ const routesQuery = useCrossChainTradeRoutes()
+
+ const {
+ state: { selectedBridge },
+ } = useDerivedStateCrossChainSwap()
+
+ const route: UseSelectedCrossChainTradeRouteReturn | undefined =
+ useMemo(() => {
+ const route = routesQuery.data?.find(
+ (route) => route.steps[0].tool === selectedBridge,
+ )
+
+ if (!route) return undefined
+
+ const tokenIn =
+ route.fromToken.address === zeroAddress
+ ? Native.onChain(route.fromToken.chainId)
+ : new Token(route.fromToken)
+
+ const tokenOut =
+ route.toToken.address === zeroAddress
+ ? Native.onChain(route.toToken.chainId)
+ : new Token(route.toToken)
- return adapter === SushiXSwap2Adapter.Stargate
- ? stargateCrossChainTrade
- : squidCrossChainTrade
+ const amountIn = Amount.fromRawAmount(tokenIn, route.fromAmount)
+ const amountOut = Amount.fromRawAmount(tokenOut, route.toAmount)
+ const amountOutMin = Amount.fromRawAmount(tokenOut, route.toAmountMin)
+
+ const fromAmountUSD =
+ (Number(route.fromToken.priceUSD) * Number(amountIn.quotient)) /
+ 10 ** tokenIn.decimals
+
+ const toAmountUSD =
+ (Number(route.toToken.priceUSD) * Number(amountOut.quotient)) /
+ 10 ** tokenOut.decimals
+
+ const priceImpact = new Percent(
+ Math.floor((fromAmountUSD / toAmountUSD - 1) * 10_000),
+ 10_000,
+ )
+
+ // const gasSpent = Amount.fromRawAmount(
+ // Native.onChain(route.fromChainId),
+ // route.steps.reduce(
+ // (total, step) =>
+ // total +
+ // step.estimate.gasCosts.reduce(
+ // (total, gasCost) => total + gasCost.amount,
+ // 0n,
+ // ),
+ // 0n,
+ // ),
+ // ).toFixed(6)
+
+ return {
+ ...route,
+ tokenIn,
+ tokenOut,
+ amountIn,
+ amountOut,
+ amountOutMin,
+ priceImpact,
+ // gasSpent,
+ }
+ }, [routesQuery.data, selectedBridge])
+
+ return useMemo(
+ () => ({
+ ...routesQuery,
+ data: route,
+ }),
+ [routesQuery, route],
+ )
}
export {
DerivedstateCrossChainSwapProvider,
- useCrossChainSwapTrade,
+ useCrossChainTradeRoutes,
+ useSelectedCrossChainTradeRoute,
useDerivedStateCrossChainSwap,
}
diff --git a/apps/web/src/ui/swap/simple/simple-swap-switch-tokens-button.tsx b/apps/web/src/ui/swap/simple/simple-swap-switch-tokens-button.tsx
index a42da4353b..aa951d1847 100644
--- a/apps/web/src/ui/swap/simple/simple-swap-switch-tokens-button.tsx
+++ b/apps/web/src/ui/swap/simple/simple-swap-switch-tokens-button.tsx
@@ -16,7 +16,7 @@ export const SimpleSwapSwitchTokensButton = () => {
} = useDerivedStateSimpleSwap()
return (
-
+
{
className="hover:shadow-sm transition-border z-10 group bg-background p-2 border border-accent transition-all rounded-full cursor-pointer"
>
diff --git a/apps/web/src/ui/swap/swap-mode-buttons.tsx b/apps/web/src/ui/swap/swap-mode-buttons.tsx
index f210e9272e..2ff987176c 100644
--- a/apps/web/src/ui/swap/swap-mode-buttons.tsx
+++ b/apps/web/src/ui/swap/swap-mode-buttons.tsx
@@ -12,9 +12,8 @@ import {
import { ShuffleIcon } from '@sushiswap/ui/icons/ShuffleIcon'
import Link from 'next/link'
import { useParams } from 'next/navigation'
-import { isTwapSupportedChainId } from 'src/config'
+import { isTwapSupportedChainId, isXSwapSupportedChainId } from 'src/config'
import { ChainId, ChainKey } from 'sushi/chain'
-import { isSushiXSwap2ChainId } from 'sushi/config'
import { PathnameButton } from '../pathname-button'
export const SwapModeButtons = () => {
@@ -49,7 +48,7 @@ export const SwapModeButtons = () => {