diff --git a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/layout.tsx b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/layout.tsx index a3048336b9..b10266f8cc 100644 --- a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/layout.tsx +++ b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/layout.tsx @@ -1,10 +1,7 @@ import { Metadata } from 'next' import { notFound } from 'next/navigation' +import { XSWAP_SUPPORTED_CHAIN_IDS, isXSwapSupportedChainId } from 'src/config' import { ChainId } from 'sushi/chain' -import { - SUSHIXSWAP_2_SUPPORTED_CHAIN_IDS, - isSushiXSwap2ChainId, -} from 'sushi/config' import { SidebarContainer } from '~evm/_common/ui/sidebar' import { Providers } from './providers' @@ -24,7 +21,7 @@ export default async function CrossChainSwapLayout(props: { const chainId = +params.chainId as ChainId - if (!isSushiXSwap2ChainId(chainId)) { + if (!isXSwapSupportedChainId(chainId)) { return notFound() } @@ -32,10 +29,12 @@ export default async function CrossChainSwapLayout(props: { -
{children}
+
+ {children} +
) diff --git a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/loading.tsx b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/loading.tsx index 82ec9e1b27..d4bc09adfc 100644 --- a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/loading.tsx +++ b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/loading.tsx @@ -7,20 +7,20 @@ export default function CrossChainSwapLoading() {
- - + +
- - - - + + + +
-
- - +
+ +
- +
) diff --git a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/page.tsx b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/page.tsx index 8093285a4f..d6a506f454 100644 --- a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/page.tsx +++ b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/cross-chain-swap/page.tsx @@ -1,10 +1,32 @@ -import { Container } from '@sushiswap/ui' +'use client' + +import { Container, classNames } from '@sushiswap/ui' +import { useSidebar } from 'src/ui/sidebar' +import { CrossChainSwapRouteSelector } from 'src/ui/swap/cross-chain/cross-chain-swap-route-selector' import { CrossChainSwapWidget } from 'src/ui/swap/cross-chain/cross-chain-swap-widget' +import { useCrossChainTradeRoutes } from 'src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider' + +export default function CrossChainSwapPage() { + const { isLoading, isFetched } = useCrossChainTradeRoutes() + const { isOpen: isSidebarOpen } = useSidebar() + + const showRouteSelector = isLoading || isFetched -export default async function CrossChainSwapPage() { return ( - - - +
+ + + + {showRouteSelector ? ( + + + + ) : null} +
) } diff --git a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/dca/loading.tsx b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/dca/loading.tsx index 97f11a2e68..7c547392cd 100644 --- a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/dca/loading.tsx +++ b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/dca/loading.tsx @@ -7,20 +7,20 @@ export default function SimpleSwapLoading() {
- - + +
- - - - + + + +
-
- - +
+ +
- +
) diff --git a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/limit/loading.tsx b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/limit/loading.tsx index 97f11a2e68..7c547392cd 100644 --- a/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/limit/loading.tsx +++ b/apps/web/src/app/(networks)/(evm)/[chainId]/(trade)/limit/loading.tsx @@ -7,20 +7,20 @@ export default function SimpleSwapLoading() {
- - + +
- - - - + + + +
-
- - +
+ +
- +
) 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 98674eb2b8..1c565ee5b4 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 => false // 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/swap/cross-chain/cross-chain-swap-confirmation-dialog.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-confirmation-dialog.tsx index 05b465d10e..3dfb59e4ff 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-confirmation-dialog.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-confirmation-dialog.tsx @@ -1,50 +1,44 @@ import { ArrowTopRightOnSquareIcon } from '@heroicons/react/20/solid' -import { Button, Currency, Dots, Loader, classNames } from '@sushiswap/ui' +import { Button, Dots, Loader, classNames } from '@sushiswap/ui' import { CheckMarkIcon } from '@sushiswap/ui/icons/CheckMarkIcon' import { FailedMarkIcon } from '@sushiswap/ui/icons/FailedMarkIcon' -import { SquidIcon } from '@sushiswap/ui/icons/SquidIcon' import { FC, ReactNode } from 'react' -import { UseCrossChainTradeReturn } from 'src/lib/hooks' -import { - SushiXSwap2Adapter, - SushiXSwapTransactionType, -} from 'src/lib/swap/cross-chain/lib' import { Chain } from 'sushi/chain' -import { STARGATE_TOKEN } from 'sushi/config' import { shortenAddress } from 'sushi/format' import { - useCrossChainSwapTrade, + UseSelectedCrossChainTradeRouteReturn, useDerivedStateCrossChainSwap, + useSelectedCrossChainTradeRoute, } from './derivedstate-cross-chain-swap-provider' interface ConfirmationDialogContent { txHash?: string dstTxHash?: string bridgeUrl?: string - adapter?: SushiXSwap2Adapter dialogState: { source: StepState; bridge: StepState; dest: StepState } - tradeRef: React.MutableRefObject + routeRef: React.MutableRefObject } export const ConfirmationDialogContent: FC = ({ txHash, bridgeUrl, - adapter, dstTxHash, dialogState, - tradeRef, + routeRef, }) => { const { state: { chainId0, chainId1, token0, token1, recipient }, } = useDerivedStateCrossChainSwap() - const { data: trade } = useCrossChainSwapTrade() + const { data: trade } = useSelectedCrossChainTradeRoute() const swapOnDest = - trade?.transactionType && + trade?.steps[0] && [ - SushiXSwapTransactionType.BridgeAndSwap, - SushiXSwapTransactionType.CrossChainSwap, - ].includes(trade.transactionType) + trade.steps[0].includedSteps[1]?.type, + trade.steps[0].includedSteps[2]?.type, + ].includes('swap') + ? true + : false if (dialogState.source === StepState.Sign) { return <>Please sign order with your wallet. @@ -98,17 +92,24 @@ export const ConfirmationDialogContent: FC = ({ {' '} - ) } 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 ? ( 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} > @@ -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)} + + )} +
+
+ + )} + +
+ +
+ {step && ( + + + + + + )} {recipient && ( @@ -624,23 +920,24 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ 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 = () => {