From bda6fe700cece048cf34fe56b6af2cbba7bc6ab6 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Thu, 12 Dec 2024 18:27:38 +0700 Subject: [PATCH 01/26] WIP: lifi xswap integration --- .../(trade)/cross-chain-swap/layout.tsx | 9 +- .../(trade)/cross-chain-swap/loading.tsx | 20 +- .../(trade)/cross-chain-swap/page.tsx | 27 +- .../(evm)/[chainId]/(trade)/dca/loading.tsx | 20 +- .../(evm)/[chainId]/(trade)/limit/loading.tsx | 20 +- .../(evm)/[chainId]/(trade)/swap/loading.tsx | 20 +- .../(networks)/(evm)/api/cross-chain/route.ts | 75 --- .../(evm)/api/cross-chain/routes/route.ts | 79 +++ .../(evm)/api/cross-chain/step/route.ts | 50 ++ apps/web/src/config.ts | 46 ++ apps/web/src/lib/hooks/api/index.ts | 1 - .../src/lib/hooks/api/useCrossChainTrade.ts | 164 ------ .../react-query/cross-chain-trade/index.ts | 2 + .../react-query/cross-chain-trade/types.ts | 15 + .../useCrossChainTradeRoutes.ts | 86 +++ .../useCrossChainTradeStep.ts | 113 ++++ apps/web/src/lib/hooks/react-query/index.ts | 1 + .../cross-chain/actions/getCrossChainTrade.ts | 91 --- .../actions/getCrossChainTrades.ts | 12 - .../actions/getSquidCrossChainTrade.ts | 373 ------------- .../swap/cross-chain/actions/getSquidRoute.ts | 25 - .../actions/getStargateCrossChainTrade.ts | 405 -------------- .../cross-chain/actions/getStargateFees.ts | 180 ------ .../lib/swap/cross-chain/actions/getTrade.ts | 56 -- .../src/lib/swap/cross-chain/actions/index.ts | 2 - .../src/lib/swap/cross-chain/hooks/index.ts | 3 +- .../cross-chain/hooks/useAxelarScanLink.ts | 56 -- .../cross-chain/hooks/useLayerZeroScanLink.ts | 50 -- .../swap/cross-chain/hooks/useLifiScanLink.ts | 59 ++ apps/web/src/lib/swap/cross-chain/index.ts | 5 +- .../lib/swap/cross-chain/lib/SquidAdapter.ts | 90 --- .../swap/cross-chain/lib/StargateAdapter.ts | 152 ----- .../lib/swap/cross-chain/lib/SushiXSwap.ts | 106 ---- .../web/src/lib/swap/cross-chain/lib/index.ts | 4 - .../web/src/lib/swap/cross-chain/lib/utils.ts | 11 - apps/web/src/lib/swap/cross-chain/schema.ts | 153 +++++ apps/web/src/lib/swap/cross-chain/types.ts | 28 + apps/web/src/lib/swap/cross-chain/utils.tsx | 79 +++ apps/web/src/lib/swap/queryParamsSchema.ts | 65 --- .../cross-chain-fees-hover-card.tsx | 128 +++++ .../cross-chain/cross-chain-route-loading.tsx | 60 ++ .../cross-chain-route-selector.tsx | 64 +++ .../ui/swap/cross-chain/cross-chain-route.tsx | 293 ++++++++++ .../cross-chain-swap-confirmation-dialog.tsx | 66 +-- .../cross-chain-swap-switch-tokens-button.tsx | 7 +- .../cross-chain-swap-token0-input.tsx | 26 +- .../cross-chain-swap-token1-input.tsx | 26 +- .../cross-chain-swap-trade-button.tsx | 11 +- .../cross-chain-swap-trade-review-dialog.tsx | 527 +++++++++++------- .../cross-chain-swap-trade-review-route.tsx | 136 +++-- .../cross-chain-swap-trade-stats.tsx | 85 +-- ...derivedstate-cross-chain-swap-provider.tsx | 255 +++++---- .../simple-swap-switch-tokens-button.tsx | 7 +- .../simple-swap-trade-review-dialog.tsx | 2 +- apps/web/src/ui/swap/swap-mode-buttons.tsx | 5 +- packages/notifications/src/types.ts | 3 +- 56 files changed, 1913 insertions(+), 2541 deletions(-) delete mode 100644 apps/web/src/app/(networks)/(evm)/api/cross-chain/route.ts create mode 100644 apps/web/src/app/(networks)/(evm)/api/cross-chain/routes/route.ts create mode 100644 apps/web/src/app/(networks)/(evm)/api/cross-chain/step/route.ts delete mode 100644 apps/web/src/lib/hooks/api/useCrossChainTrade.ts create mode 100644 apps/web/src/lib/hooks/react-query/cross-chain-trade/index.ts create mode 100644 apps/web/src/lib/hooks/react-query/cross-chain-trade/types.ts create mode 100644 apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeRoutes.ts create mode 100644 apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeStep.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/actions/getCrossChainTrade.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/actions/getCrossChainTrades.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/actions/getSquidCrossChainTrade.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/actions/getSquidRoute.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/actions/getStargateCrossChainTrade.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/actions/getStargateFees.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/actions/getTrade.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/actions/index.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/hooks/useAxelarScanLink.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/hooks/useLayerZeroScanLink.ts create mode 100644 apps/web/src/lib/swap/cross-chain/hooks/useLifiScanLink.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/lib/SquidAdapter.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/lib/StargateAdapter.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/lib/SushiXSwap.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/lib/index.ts delete mode 100644 apps/web/src/lib/swap/cross-chain/lib/utils.ts create mode 100644 apps/web/src/lib/swap/cross-chain/schema.ts create mode 100644 apps/web/src/lib/swap/cross-chain/types.ts create mode 100644 apps/web/src/lib/swap/cross-chain/utils.tsx delete mode 100644 apps/web/src/lib/swap/queryParamsSchema.ts create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-route-loading.tsx create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-route.tsx 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 298fd6b1d9..7a1d91db48 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' @@ -20,7 +17,7 @@ export default function CrossChainSwapLayout({ }: { children: React.ReactNode; params: { chainId: string } }) { const chainId = +params.chainId as ChainId - if (!isSushiXSwap2ChainId(chainId)) { + if (!isXSwapSupportedChainId(chainId)) { return notFound() } @@ -28,7 +25,7 @@ export default function CrossChainSwapLayout({
{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..b9989552d8 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,27 @@ -import { Container } from '@sushiswap/ui' +'use client' + +import { Container, classNames } from '@sushiswap/ui' +import { CrossChainRouteSelector } from 'src/ui/swap/cross-chain/cross-chain-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 { data: routes, isLoading } = useCrossChainTradeRoutes() + const showRouteSelector = isLoading || (routes && routes.length > 0) -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..62b2168ec6 --- /dev/null +++ b/apps/web/src/app/(networks)/(evm)/api/cross-chain/routes/route.ts @@ -0,0 +1,79 @@ +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 +}) + +export const revalidate = 600 + +export async function GET(request: NextRequest) { + const params = Object.fromEntries(request.nextUrl.searchParams.entries()) + + const { slippage, ...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, + integrator: 'sushi', + exchanges: { allow: ['sushiswap'] }, + allowSwitchChain: false, + allowDestinationCall: true, + order: 'CHEAPEST', + // 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': 'max-age=60, stale-while-revalidate=600', + }, + }) +} 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..ed06e8b9d3 --- /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': 'max-age=10, stale-while-revalidate=60', + }, + }) +} diff --git a/apps/web/src/config.ts b/apps/web/src/config.ts index 5de40159c4..ae95bc52f3 100644 --- a/apps/web/src/config.ts +++ b/apps/web/src/config.ts @@ -117,6 +117,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, @@ -223,3 +236,36 @@ export const isZapSupportedChainId = ( chainId: number, ): chainId is ZapSupportedChainId => ZAP_SUPPORTED_CHAIN_IDS.includes(chainId as ZapSupportedChainId) + +export const XSWAP_SUPPORTED_CHAIN_IDS = [ + ChainId.ARBITRUM, + ChainId.AVALANCHE, + ChainId.BSC, + ChainId.BASE, + ChainId.BLAST, + ChainId.BOBA, + ChainId.CELO, + ChainId.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..e1c4f23c79 --- /dev/null +++ b/apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeRoutes.ts @@ -0,0 +1,86 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query' +import { useDerivedStateCrossChainSwap } from 'src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider' +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 + query?: Omit, 'queryFn' | 'queryKey'> +} + +export const useCrossChainTradeRoutes = ({ + query, + ...params +}: UseCrossChainTradeRoutesParms) => { + const { + mutate: { setRouteIndex }, + } = useDerivedStateCrossChainSwap() + + return useQuery({ + queryKey: ['cross-chain/routes', params], + queryFn: async (): Promise => { + const { fromAmount, toToken, slippage } = params + + if (!fromAmount || !toToken) throw new Error() + + setRouteIndex(0) + + 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, + )) + + 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 + }, + staleTime: query?.staleTime ?? 1000 * 15, // 15s + 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..0b55e512eb --- /dev/null +++ b/apps/web/src/lib/hooks/react-query/cross-chain-trade/useCrossChainTradeStep.ts @@ -0,0 +1,113 @@ +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, + // slippage: step.action.slippage // TODO: CHECK IF NEEDED HERE + }), + } + + 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..828faa597c --- /dev/null +++ b/apps/web/src/lib/swap/cross-chain/schema.ts @@ -0,0 +1,153 @@ +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), + 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..459d9cf982 --- /dev/null +++ b/apps/web/src/lib/swap/cross-chain/types.ts @@ -0,0 +1,28 @@ +import { z } from 'zod' +import { + crossChainActionSchema, + crossChainRouteSchema, + crossChainStepSchema, + crossChainToolDetailsSchema, + crossChainTransactionRequestSchema, +} from './schema' + +type CrossChainAction = z.infer + +type CrossChainRoute = z.infer + +type CrossChainStep = z.infer + +type CrossChainToolDetails = z.infer + +type CrossChainTransactionRequest = z.infer< + typeof crossChainTransactionRequestSchema +> + +export type { + CrossChainAction, + CrossChainRoute, + CrossChainStep, + CrossChainToolDetails, + CrossChainTransactionRequest, +} 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/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx new file mode 100644 index 0000000000..901124866b --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx @@ -0,0 +1,128 @@ +import { + Card, + CardContent, + HoverCard, + HoverCardContent, + HoverCardTrigger, + Popover, + PopoverContent, + PopoverTrigger, +} from '@sushiswap/ui' +import { FC, useMemo } from 'react' +import { FeesBreakdown } from 'src/lib/swap/cross-chain' +import { ChainId } from 'sushi/chain' +import { formatUSD } from 'sushi/format' + +interface CrossChainFeesHoverCardProps { + feesBreakdown: FeesBreakdown + gasFeesUSD: number + protocolFeesUSD: number + chainId0: ChainId + chainId1: ChainId + children: React.ReactNode +} + +export const CrossChainFeesHoverCard: FC = ({ + feesBreakdown, + gasFeesUSD, + protocolFeesUSD, + chainId0, + chainId1, + children, +}) => { + const content = useMemo(() => { + return ( + + + {feesBreakdown.gas.size > 0 ? ( +
+
+ Network Fees + {formatUSD(gasFeesUSD)} +
+ {feesBreakdown.gas.get(chainId0) ? ( +
+ Origin Chain + + {feesBreakdown.gas.get(chainId0)!.amount.toSignificant(4)}{' '} + {feesBreakdown.gas.get(chainId0)!.amount.currency.symbol} + +
+ ) : null} + {feesBreakdown.gas.get(chainId1) ? ( +
+ Dest. Chain + + {feesBreakdown.gas.get(chainId1)!.amount.toSignificant(4)}{' '} + {feesBreakdown.gas.get(chainId1)!.amount.currency.symbol} + +
+ ) : null} +
+ ) : null} + {feesBreakdown.protocol.size > 0 ? ( +
+
+ Protocol Fees + + {formatUSD(protocolFeesUSD)} + +
+ {feesBreakdown.protocol.get(chainId0) ? ( +
+ Origin Chain + + {feesBreakdown.protocol + .get(chainId0)! + .amount.toSignificant(4)}{' '} + { + feesBreakdown.protocol.get(chainId0)!.amount.currency + .symbol + } + +
+ ) : null} + {feesBreakdown.protocol.get(chainId1) ? ( +
+ Dest. Chain + + {feesBreakdown.protocol + .get(chainId1)! + .amount.toSignificant(4)}{' '} + { + feesBreakdown.protocol.get(chainId1)!.amount.currency + .symbol + } + +
+ ) : null} +
+ ) : null} +
+
+ ) + }, [feesBreakdown, chainId0, chainId1, gasFeesUSD, protocolFeesUSD]) + + return ( + <> +
+ + {children} + + {content} + + +
+
+ + e.stopPropagation()} asChild> + {children} + + + {content} + + +
+ + ) +} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-loading.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-loading.tsx new file mode 100644 index 0000000000..04ae79ac0d --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-route-loading.tsx @@ -0,0 +1,60 @@ +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Separator, + SkeletonBox, + SkeletonText, +} from '@sushiswap/ui' +import { FC } from 'react' + +interface CrossChainRouteLoadingProps { + isSelected?: boolean +} + +export const CrossChainRouteLoading: FC = ({ + isSelected = false, +}) => { + return ( + + + +
+ +
+
+ +
+ +
+
+
+ {isSelected ? ( + <> + + + + + + + ) : null} + +
+
+ +
+ +
+ +
+
+
+
+ ) +} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx new file mode 100644 index 0000000000..2760beb684 --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx @@ -0,0 +1,64 @@ +'use client' + +import { + Card, + CardContent, + CardHeader, + CardTitle, + Collapsible, +} from '@sushiswap/ui' +import { FC } from 'react' +import { CrossChainRoute as CrossChainRouteType } from 'src/lib/swap/cross-chain/types' +import { CrossChainRoute } from './cross-chain-route' +import { CrossChainRouteLoading } from './cross-chain-route-loading' +import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' + +interface CrossChainRouteSelectorProps { + routes: CrossChainRouteType[] | undefined + isLoading: boolean +} + +export const CrossChainRouteSelector: FC = ({ + routes, + isLoading, +}) => { + const { + state: { routeIndex }, + mutate: { setRouteIndex }, + } = useDerivedStateCrossChainSwap() + + return ( +
+ + + + Select A Route + + + + {isLoading ? ( + <> + + {Array.from({ length: 3 }).map((_, index) => ( + + ))} + + ) : routes?.length ? ( + routes.map((route, index) => ( + setRouteIndex(index)} + /> + )) + ) : null} + + + +
+ ) +} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route.tsx new file mode 100644 index 0000000000..45185ca6b5 --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-route.tsx @@ -0,0 +1,293 @@ +import { ChevronDoubleRightIcon, ClockIcon } from '@heroicons/react/24/outline' +import { ArrowsRightLeftIcon } from '@heroicons/react/24/solid' +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + Currency, + Separator, + SkeletonBox, + SkeletonText, + classNames, +} from '@sushiswap/ui' +import { GasIcon } from '@sushiswap/ui/icons/GasIcon' +import React, { FC, useMemo } from 'react' +import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain' +import { + CrossChainAction as CrossChainActionType, + CrossChainRoute as CrossChainRouteType, + CrossChainToolDetails as CrossChainToolDetailsType, +} from 'src/lib/swap/cross-chain/types' +import { Chain, ChainId } from 'sushi/chain' +import { Amount, Native, Token } from 'sushi/currency' +import { formatUSD } from 'sushi/format' +import { zeroAddress } from 'viem' +import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price' +import { CrossChainFeesHoverCard } from './cross-chain-fees-hover-card' +import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' + +const SwapAction: FC<{ + action: CrossChainActionType + chainId0: ChainId +}> = ({ chainId0, action }) => { + const { fromToken, toToken, label, chain } = useMemo(() => { + const [label, chain] = + chainId0 === action.fromToken.chainId + ? [ + 'From', + Chain.fromChainId(action.fromToken.chainId)?.name?.toUpperCase(), + ] + : ['To', Chain.fromChainId(action.toToken.chainId)?.name?.toUpperCase()] + + return { + fromToken: + action.fromToken.address === zeroAddress + ? Native.onChain(action.fromToken.chainId) + : new Token(action.fromToken), + toToken: + action.toToken.address === zeroAddress + ? Native.onChain(action.toToken.chainId) + : new Token(action.toToken), + label, + chain, + } + }, [action.fromToken, action.toToken, chainId0]) + + return ( +
+ + {label}: {chain} + +
+
+ +
+
+ +
+
+ +
+
+ + Swap {fromToken.symbol} to {toToken.symbol} + +
+ ) +} + +const BridgeAction: FC<{ + action: CrossChainActionType + toolDetails: CrossChainToolDetailsType +}> = ({ action, toolDetails }) => { + return ( +
+ + Via{' '} + {toolDetails.name}{' '} + {toolDetails.name} + +
+
+ +
+
+ + Bridge {action.fromToken.symbol} + +
+ ) +} + +interface CrossChainRouteProps { + route: CrossChainRouteType + isSelected: boolean + onSelect: () => void +} + +export const CrossChainRoute: FC = ({ + route, + isSelected, + onSelect, +}) => { + const { + state: { token1, chainId0, chainId1 }, + } = useDerivedStateCrossChainSwap() + + const { data: price } = usePrice({ + chainId: token1?.chainId, + address: token1?.wrapped.address, + }) + + const amountOut = useMemo( + () => + route?.toAmount && token1 + ? Amount.fromRawAmount(token1, route.toAmount) + : undefined, + [token1, route?.toAmount], + ) + + const amountOutUSD = useMemo( + () => + price && amountOut + ? `${( + (price * Number(amountOut.quotient)) / + 10 ** amountOut.currency.decimals + ).toFixed(2)}` + : undefined, + [amountOut, price], + ) + + const { + step, + executionDuration, + feesBreakdown, + gasFeesUSD, + protocolFeesUSD, + totalFeesUSD, + } = useMemo(() => { + const step = route.steps[0] + const executionDurationSeconds = step.estimate.executionDuration + const executionDurationMinutes = Math.floor(executionDurationSeconds / 60) + + const executionDuration = + executionDurationSeconds < 60 + ? `${executionDurationSeconds} seconds` + : `${executionDurationMinutes} minutes` + + const { feesBreakdown, totalFeesUSD, gasFeesUSD, protocolFeesUSD } = + getCrossChainFeesBreakdown(route.steps) + + return { + step, + executionDuration, + feesBreakdown, + totalFeesUSD, + gasFeesUSD, + protocolFeesUSD, + } + }, [route?.steps]) + + return ( + + + + {amountOut && token1 ? ( + `${amountOut?.toSignificant(6)} ${token1?.symbol}` + ) : ( +
+ +
+ )} +
+ {`≈ ${amountOutUSD}`} +
+ {isSelected ? ( + <> + + + {step ? ( +
+ {step.includedSteps.map((_step, index) => { + return ( + + {_step.type === 'swap' ? ( + + ) : _step.type === 'cross' ? ( + + ) : null} + {index < step.includedSteps.length - 1 ? ( +
+ + + + + +
+ ) : null} +
+ ) + })} +
+ ) : ( + + )} +
+ + + ) : null} + +
+
+ + + {executionDuration} + + + + + {formatUSD(totalFeesUSD)} + + +
+ + {step.toolDetails.name} + {step.toolDetails.name} + +
+
+
+ ) +} 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..1967181b52 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,18 +1,12 @@ 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 { + UseCrossChainTradeRouteReturn, useCrossChainSwapTrade, useDerivedStateCrossChainSwap, } from './derivedstate-cross-chain-swap-provider' @@ -21,18 +15,16 @@ 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 }, @@ -40,11 +32,13 @@ export const ConfirmationDialogContent: FC = ({ const { data: trade } = useCrossChainSwapTrade() 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 }, @@ -26,21 +23,6 @@ export const CrossChainSwapToken1Input = () => { 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, - ]), - ), - [], - ) - return ( { const { state: { swapAmount, swapAmountString, chainId0 }, } = useDerivedStateCrossChainSwap() - const { data: trade } = useCrossChainSwapTrade() + const { data: trade, isError } = useCrossChainSwapTrade() const [checked, setChecked] = useState(false) // Reset @@ -44,16 +43,14 @@ export const CrossChainSwapTradeButton: FC = () => { id="approve-erc20" fullWidth amount={swapAmount} - contract={ - SUSHIXSWAP_2_ADDRESS[chainId0 as SushiXSwap2ChainId] - } + contract={trade?.steps?.[0]?.estimate?.approvalAddress} > diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx index 59de4989c6..cda8081307 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx @@ -41,28 +41,19 @@ import React, { useRef, useState, } from 'react' +import { isXSwapSupportedChainId } from 'src/config' import { APPROVE_TAG_XSWAP } from 'src/lib/constants' -import { UseCrossChainTradeReturn } from 'src/lib/hooks' +import { useCrossChainTradeStep } from 'src/lib/hooks/react-query' import { useSlippageTolerance } from 'src/lib/hooks/useSlippageTolerance' import { - SushiXSwap2Adapter, - SushiXSwapFunctionName, - SushiXSwapWriteArgsBridge, - SushiXSwapWriteArgsSwapAndBridge, - useAxelarScanLink, - useLayerZeroScanLink, + getCrossChainFeesBreakdown, + useLiFiStatus, } from 'src/lib/swap/cross-chain' import { warningSeverity } from 'src/lib/swap/warningSeverity' import { useApproved } from 'src/lib/wagmi/systems/Checker/Provider' -import { sushiXSwap2Abi_bridge, sushiXSwap2Abi_swapAndBridge } from 'sushi/abi' -import { Chain, chainName } from 'sushi/chain' -import { - SUSHIXSWAP_2_ADDRESS, - SushiXSwap2ChainId, - isSushiXSwap2ChainId, -} from 'sushi/config' +import { Chain, ChainKey, chainName } from 'sushi/chain' import { Native } from 'sushi/currency' -import { shortenAddress } from 'sushi/format' +import { formatUSD, shortenAddress } from 'sushi/format' import { ZERO } from 'sushi/math' import { SendTransactionReturnType, @@ -71,10 +62,10 @@ import { } from 'viem' import { useAccount, + useEstimateGas, usePublicClient, - useSimulateContract, + useSendTransaction, useTransaction, - useWriteContract, } from 'wagmi' import { useRefetchBalances } from '~evm/_common/ui/balance-provider/use-refetch-balances' import { @@ -87,32 +78,11 @@ import { } from './cross-chain-swap-confirmation-dialog' import { CrossChainSwapTradeReviewRoute } from './cross-chain-swap-trade-review-route' import { + UseCrossChainTradeRouteReturn, useCrossChainSwapTrade, useDerivedStateCrossChainSwap, } from './derivedstate-cross-chain-swap-provider' -function getConfig(trade: UseCrossChainTradeReturn | undefined) { - if (!trade) return {} - - if (trade.functionName === SushiXSwapFunctionName.Bridge) { - return { - abi: sushiXSwap2Abi_bridge, - functionName: 'bridge', - args: trade.writeArgs as NonNullable, - 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, }) => { @@ -121,7 +91,6 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ const { mutate: { setTradeId, setSwapAmount }, state: { - adapter, recipient, swapAmount, swapAmountString, @@ -134,8 +103,19 @@ 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 { data: trade } = useCrossChainSwapTrade() + const { + data: step, + isFetching, + isError: isStepQueryError, + } = useCrossChainTradeStep({ + step: trade?.steps?.[0], + query: { + enabled: Boolean(approved && address), + }, + }) + const groupTs = useRef() const { refetchChain: refetchBalances } = useRefetchBalances() @@ -149,39 +129,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 +181,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 +197,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 +253,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 +308,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 && trade) { + routeRef.current = trade } }, }, }) - // 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 +336,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,33 +417,61 @@ 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 feeData = useMemo( + () => (step ? getCrossChainFeesBreakdown([step]) : undefined), + [step], + ) return ( @@ -498,7 +482,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ 0 && - stringify(error).includes('insufficient funds'), + stringify(estGasError).includes('insufficient funds'), )} >
@@ -507,7 +491,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} @@ -524,7 +508,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ {isFetching ? ( ) : ( - `Receive ${trade?.amountOut?.toSignificant(6)} ${ + `Receive ${step?.amountOut?.toSignificant(6)} ${ token1?.symbol }` )} @@ -556,7 +540,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ title="Price impact" subtitle="The impact your trade has on the market price of this pool." > - {isFetching || !trade?.priceImpact ? ( + {isFetching || !step?.priceImpact ? ( = ({ /> ) : ( `${ - trade?.priceImpact?.lessThan(ZERO) + step.priceImpact.lessThan(ZERO) ? '+' - : trade?.priceImpact?.greaterThan(ZERO) + : step.priceImpact.greaterThan(ZERO) ? '-' : '' - }${Math.abs(Number(trade?.priceImpact?.toFixed(2)))}%` + }${Math.abs(Number(step.priceImpact.toFixed(2)))}%` + )} + + + {isFetching || !step?.amountOut ? ( + + ) : ( + `${step.amountOut.toSignificant(6)} ${token1?.symbol}` )} - {isFetching || !trade?.amountOutMin ? ( + {isFetching || !step?.amountOutMin ? ( ) : ( - `${trade?.amountOutMin?.toSignificant(6)} ${ + `${step.amountOutMin?.toSignificant(6)} ${ token1?.symbol }` )} +
+
+ Fees +
+ {feeData ? ( +
+ {feeData.feesBreakdown.gas.size > 0 ? ( +
+ Network Fees + {feeData.feesBreakdown.gas.get(chainId0) ? ( + Origin Chain + ) : null} + {feeData.feesBreakdown.gas.get(chainId1) ? ( + Dest. Chain + ) : null} +
+ ) : null} + {feeData.feesBreakdown.protocol.size > 0 ? ( +
+ Protocol Fees + {feeData.feesBreakdown.protocol.get( + chainId0, + ) ? ( + Origin Chain + ) : null} + + {feeData.feesBreakdown.protocol.get( + chainId1, + ) ? ( + Dest. Chain + ) : null} +
+ ) : null} +
+ ) : null} +
+
+
+ + {feeData && + feeData.totalFeesUSD !== feeData.gasFeesUSD && + feeData.totalFeesUSD !== feeData.protocolFeesUSD + ? formatUSD(feeData.totalFeesUSD) + : ''} + +
+ {feeData ? ( +
+ {feeData.feesBreakdown.gas.size > 0 ? ( +
+ {formatUSD(feeData.gasFeesUSD)} + {feeData.feesBreakdown.gas.get(chainId0) ? ( + + {feeData.feesBreakdown.gas + .get(chainId0)! + .amount.toSignificant(4)}{' '} + { + feeData.feesBreakdown.gas.get(chainId0)! + .amount.currency.symbol + } + + ) : null} + {feeData.feesBreakdown.gas.get(chainId1) ? ( + + {feeData.feesBreakdown.gas + .get(chainId1)! + .amount.toSignificant(4)}{' '} + { + feeData.feesBreakdown.gas.get(chainId1)! + .amount.currency.symbol + } + + ) : null} +
+ ) : null} + {feeData.feesBreakdown.protocol.size > 0 ? ( +
+ + {formatUSD(feeData.protocolFeesUSD)} + + {feeData.feesBreakdown.protocol.get( + chainId0, + ) ? ( + + {feeData.feesBreakdown.protocol + .get(chainId0)! + .amount.toSignificant(4)}{' '} + { + feeData.feesBreakdown.protocol.get( + chainId0, + )!.amount.currency.symbol + } + + ) : null} + {feeData.feesBreakdown.protocol.get( + chainId1, + ) ? ( + + {feeData.feesBreakdown.protocol + .get(chainId1)! + .amount.toSignificant(4)}{' '} + { + feeData.feesBreakdown.protocol.get( + chainId1, + )!.amount.currency.symbol + } + + ) : null} +
+ ) : null} +
+ ) : null} +
+
+
@@ -624,23 +741,24 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ diff --git a/apps/web/src/ui/swap/simple/simple-swap-trade-review-dialog.tsx b/apps/web/src/ui/swap/simple/simple-swap-trade-review-dialog.tsx index a23e0e24c6..d3a31b3baf 100644 --- a/apps/web/src/ui/swap/simple/simple-swap-trade-review-dialog.tsx +++ b/apps/web/src/ui/swap/simple/simple-swap-trade-review-dialog.tsx @@ -335,7 +335,7 @@ export const SimpleSwapTradeReviewDialog: FC<{ : '' }${Math.abs( Number(trade?.priceImpact?.toFixed(2)), - )}%` ?? '-' + )}%` )} diff --git a/apps/web/src/ui/swap/swap-mode-buttons.tsx b/apps/web/src/ui/swap/swap-mode-buttons.tsx index 61a5b74759..91daf73b26 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 = () => { Date: Fri, 13 Dec 2024 21:49:28 +0700 Subject: [PATCH 02/26] feat: xswap route sorting --- .../(evm)/api/cross-chain/routes/route.ts | 5 +- .../useCrossChainTradeRoutes.ts | 9 +- apps/web/src/lib/swap/cross-chain/schema.ts | 1 + apps/web/src/lib/swap/cross-chain/types.ts | 3 + .../cross-chain-route-card-loading.tsx | 47 +++ .../cross-chain/cross-chain-route-card.tsx | 171 ++++++++++ .../cross-chain/cross-chain-route-loading.tsx | 60 ---- .../cross-chain-route-selector.tsx | 43 ++- .../ui/swap/cross-chain/cross-chain-route.tsx | 293 ------------------ .../cross-chain-swap-confirmation-dialog.tsx | 8 +- .../cross-chain-swap-token1-input.tsx | 10 +- .../cross-chain-swap-trade-button.tsx | 20 +- .../cross-chain-swap-trade-review-dialog.tsx | 14 +- .../cross-chain-swap-trade-review-route.tsx | 4 +- .../cross-chain-swap-trade-stats.tsx | 4 +- ...derivedstate-cross-chain-swap-provider.tsx | 154 +++++---- 16 files changed, 376 insertions(+), 470 deletions(-) create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-route-card-loading.tsx create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx delete mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-route-loading.tsx delete mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-route.tsx 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 index 62b2168ec6..9a4f52fc24 100644 --- 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 @@ -34,6 +34,7 @@ const schema = z.object({ }) .optional(), slippage: z.coerce.number(), // decimal + order: z.enum(['CHEAPEST', 'FASTEST']).optional(), }) export const revalidate = 600 @@ -41,7 +42,7 @@ export const revalidate = 600 export async function GET(request: NextRequest) { const params = Object.fromEntries(request.nextUrl.searchParams.entries()) - const { slippage, ...parsedParams } = schema.parse(params) + const { slippage, order = 'CHEAPEST', ...parsedParams } = schema.parse(params) const url = new URL('https://li.quest/v1/advanced/routes') @@ -58,11 +59,11 @@ export async function GET(request: NextRequest) { ...parsedParams, options: { slippage, + order, integrator: 'sushi', exchanges: { allow: ['sushiswap'] }, allowSwitchChain: false, allowDestinationCall: true, - order: 'CHEAPEST', // fee: // TODO: must set up feeReceiver w/ lifi }, }), 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 index e1c4f23c79..3ae8bbbf1f 100644 --- 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 @@ -1,5 +1,4 @@ import { UseQueryOptions, useQuery } from '@tanstack/react-query' -import { useDerivedStateCrossChainSwap } from 'src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider' import { Amount, Type } from 'sushi/currency' import { Percent } from 'sushi/math' import { Address, zeroAddress } from 'viem' @@ -17,6 +16,7 @@ export interface UseCrossChainTradeRoutesParms { fromAddress?: Address toAddress?: Address slippage: Percent + order?: 'CHEAPEST' | 'FASTEST' query?: Omit, 'queryFn' | 'queryKey'> } @@ -24,10 +24,6 @@ export const useCrossChainTradeRoutes = ({ query, ...params }: UseCrossChainTradeRoutesParms) => { - const { - mutate: { setRouteIndex }, - } = useDerivedStateCrossChainSwap() - return useQuery({ queryKey: ['cross-chain/routes', params], queryFn: async (): Promise => { @@ -35,8 +31,6 @@ export const useCrossChainTradeRoutes = ({ if (!fromAmount || !toToken) throw new Error() - setRouteIndex(0) - const url = new URL('/api/cross-chain/routes', window.location.origin) url.searchParams.set( @@ -64,6 +58,7 @@ export const useCrossChainTradeRoutes = ({ 'toAddress', params.toAddress || params.fromAddress, )) + params.order && url.searchParams.set('order', params.order) const response = await fetch(url) diff --git a/apps/web/src/lib/swap/cross-chain/schema.ts b/apps/web/src/lib/swap/cross-chain/schema.ts index 828faa597c..f684d5fadb 100644 --- a/apps/web/src/lib/swap/cross-chain/schema.ts +++ b/apps/web/src/lib/swap/cross-chain/schema.ts @@ -149,5 +149,6 @@ export const crossChainRouteSchema = z.object({ 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 index 459d9cf982..7cd70ce816 100644 --- a/apps/web/src/lib/swap/cross-chain/types.ts +++ b/apps/web/src/lib/swap/cross-chain/types.ts @@ -19,10 +19,13 @@ type CrossChainTransactionRequest = z.infer< typeof crossChainTransactionRequestSchema > +type CrossChainRouteOrder = 'CHEAPEST' | 'FASTEST' + export type { CrossChainAction, CrossChainRoute, CrossChainStep, CrossChainToolDetails, CrossChainTransactionRequest, + CrossChainRouteOrder, } diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-card-loading.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-card-loading.tsx new file mode 100644 index 0000000000..bb85b58bf0 --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-route-card-loading.tsx @@ -0,0 +1,47 @@ +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + SkeletonText, +} from '@sushiswap/ui' +import { FC } from 'react' + +interface CrossChainRouteCardLoadingProps { + isSelected?: boolean +} + +export const CrossChainRouteCardLoading: FC = + () => { + return ( + + + +
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+ +
+
+
+
+ ) + } diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx new file mode 100644 index 0000000000..a99cadbaeb --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx @@ -0,0 +1,171 @@ +import { ClockIcon } from '@heroicons/react/24/outline' +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + SkeletonText, + classNames, +} from '@sushiswap/ui' +import { GasIcon } from '@sushiswap/ui/icons/GasIcon' +import React, { FC, useMemo } from 'react' +import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain' +import { + CrossChainRoute as CrossChainRouteType, + CrossChainRouteOrder, +} from 'src/lib/swap/cross-chain/types' +import { Amount } from 'sushi/currency' +import { formatUSD } from 'sushi/format' +import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price' +import { CrossChainFeesHoverCard } from './cross-chain-fees-hover-card' +import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' + +interface CrossChainRouteCardProps { + route: CrossChainRouteType + order: CrossChainRouteOrder + isSelected: boolean + onSelect: () => void +} + +export const CrossChainRouteCard: FC = ({ + route, + order, + isSelected, + onSelect, +}) => { + const { + state: { token1, chainId0, chainId1 }, + } = useDerivedStateCrossChainSwap() + + const { data: price } = usePrice({ + chainId: token1?.chainId, + address: token1?.wrapped.address, + }) + + const amountOut = useMemo( + () => + route?.toAmount && token1 + ? Amount.fromRawAmount(token1, route.toAmount) + : undefined, + [token1, route?.toAmount], + ) + + const amountOutUSD = useMemo( + () => + price && amountOut + ? `${( + (price * Number(amountOut.quotient)) / + 10 ** amountOut.currency.decimals + ).toFixed(2)}` + : undefined, + [amountOut, price], + ) + + const { + step, + executionDuration, + feesBreakdown, + gasFeesUSD, + protocolFeesUSD, + totalFeesUSD, + } = useMemo(() => { + const step = route.steps[0] + const executionDurationSeconds = step.estimate.executionDuration + const executionDurationMinutes = Math.floor(executionDurationSeconds / 60) + + const executionDuration = + executionDurationSeconds < 60 + ? `${executionDurationSeconds} seconds` + : `${executionDurationMinutes} minutes` + + const { feesBreakdown, totalFeesUSD, gasFeesUSD, protocolFeesUSD } = + getCrossChainFeesBreakdown(route.steps) + + return { + step, + executionDuration, + feesBreakdown, + totalFeesUSD, + gasFeesUSD, + protocolFeesUSD, + } + }, [route?.steps]) + + return ( + + +
+
+ + {amountOut && token1 ? ( + `${amountOut?.toSignificant(6)} ${token1?.symbol}` + ) : ( +
+ +
+ )} +
+ {`≈ ${amountOutUSD} after fees`} +
+ + {route.tags?.includes(order) ? ( +
+ + {order === 'CHEAPEST' ? 'Best Return' : 'Fastest'} + +
+ ) : route.tags?.includes( + order === 'FASTEST' ? 'CHEAPEST' : 'FASTEST', + ) ? ( +
+ + {order === 'FASTEST' ? 'Best Return' : 'Fastest'} + +
+ ) : null} +
+
+
+ +
+
+ + + {executionDuration} + + + + + {formatUSD(totalFeesUSD)} + + +
+ + {step.toolDetails.name} + {step.toolDetails.name} + +
+
+
+ ) +} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-loading.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-loading.tsx deleted file mode 100644 index 04ae79ac0d..0000000000 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-route-loading.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Separator, - SkeletonBox, - SkeletonText, -} from '@sushiswap/ui' -import { FC } from 'react' - -interface CrossChainRouteLoadingProps { - isSelected?: boolean -} - -export const CrossChainRouteLoading: FC = ({ - isSelected = false, -}) => { - return ( - - - -
- -
-
- -
- -
-
-
- {isSelected ? ( - <> - - - - - - - ) : null} - -
-
- -
- -
- -
-
-
-
- ) -} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx index 2760beb684..55ddae7165 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx @@ -6,11 +6,16 @@ import { CardHeader, CardTitle, Collapsible, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, } from '@sushiswap/ui' import { FC } from 'react' import { CrossChainRoute as CrossChainRouteType } from 'src/lib/swap/cross-chain/types' -import { CrossChainRoute } from './cross-chain-route' -import { CrossChainRouteLoading } from './cross-chain-route-loading' +import { CrossChainRouteCard } from './cross-chain-route-card' +import { CrossChainRouteCardLoading } from './cross-chain-route-card-loading' import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' interface CrossChainRouteSelectorProps { @@ -23,8 +28,8 @@ export const CrossChainRouteSelector: FC = ({ isLoading, }) => { const { - state: { routeIndex }, - mutate: { setRouteIndex }, + state: { routeOrder, selectedBridge }, + mutate: { setRouteOrder, setSelectedBridge }, } = useDerivedStateCrossChainSwap() return ( @@ -35,24 +40,40 @@ export const CrossChainRouteSelector: FC = ({ className="bg-gray-50 dark:bg-slate-800 h-[520px] w-full overflow-hidden flex flex-col" > - Select A Route +
+ Select A Route +
+ +
+
{isLoading ? ( <> - + {Array.from({ length: 3 }).map((_, index) => ( - + ))} ) : routes?.length ? ( - routes.map((route, index) => ( - ( + setRouteIndex(index)} + order={routeOrder} + isSelected={route.steps[0].tool === selectedBridge} + onSelect={() => setSelectedBridge(route.steps[0].tool)} /> )) ) : null} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route.tsx deleted file mode 100644 index 45185ca6b5..0000000000 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-route.tsx +++ /dev/null @@ -1,293 +0,0 @@ -import { ChevronDoubleRightIcon, ClockIcon } from '@heroicons/react/24/outline' -import { ArrowsRightLeftIcon } from '@heroicons/react/24/solid' -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - Currency, - Separator, - SkeletonBox, - SkeletonText, - classNames, -} from '@sushiswap/ui' -import { GasIcon } from '@sushiswap/ui/icons/GasIcon' -import React, { FC, useMemo } from 'react' -import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain' -import { - CrossChainAction as CrossChainActionType, - CrossChainRoute as CrossChainRouteType, - CrossChainToolDetails as CrossChainToolDetailsType, -} from 'src/lib/swap/cross-chain/types' -import { Chain, ChainId } from 'sushi/chain' -import { Amount, Native, Token } from 'sushi/currency' -import { formatUSD } from 'sushi/format' -import { zeroAddress } from 'viem' -import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price' -import { CrossChainFeesHoverCard } from './cross-chain-fees-hover-card' -import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' - -const SwapAction: FC<{ - action: CrossChainActionType - chainId0: ChainId -}> = ({ chainId0, action }) => { - const { fromToken, toToken, label, chain } = useMemo(() => { - const [label, chain] = - chainId0 === action.fromToken.chainId - ? [ - 'From', - Chain.fromChainId(action.fromToken.chainId)?.name?.toUpperCase(), - ] - : ['To', Chain.fromChainId(action.toToken.chainId)?.name?.toUpperCase()] - - return { - fromToken: - action.fromToken.address === zeroAddress - ? Native.onChain(action.fromToken.chainId) - : new Token(action.fromToken), - toToken: - action.toToken.address === zeroAddress - ? Native.onChain(action.toToken.chainId) - : new Token(action.toToken), - label, - chain, - } - }, [action.fromToken, action.toToken, chainId0]) - - return ( -
- - {label}: {chain} - -
-
- -
-
- -
-
- -
-
- - Swap {fromToken.symbol} to {toToken.symbol} - -
- ) -} - -const BridgeAction: FC<{ - action: CrossChainActionType - toolDetails: CrossChainToolDetailsType -}> = ({ action, toolDetails }) => { - return ( -
- - Via{' '} - {toolDetails.name}{' '} - {toolDetails.name} - -
-
- -
-
- - Bridge {action.fromToken.symbol} - -
- ) -} - -interface CrossChainRouteProps { - route: CrossChainRouteType - isSelected: boolean - onSelect: () => void -} - -export const CrossChainRoute: FC = ({ - route, - isSelected, - onSelect, -}) => { - const { - state: { token1, chainId0, chainId1 }, - } = useDerivedStateCrossChainSwap() - - const { data: price } = usePrice({ - chainId: token1?.chainId, - address: token1?.wrapped.address, - }) - - const amountOut = useMemo( - () => - route?.toAmount && token1 - ? Amount.fromRawAmount(token1, route.toAmount) - : undefined, - [token1, route?.toAmount], - ) - - const amountOutUSD = useMemo( - () => - price && amountOut - ? `${( - (price * Number(amountOut.quotient)) / - 10 ** amountOut.currency.decimals - ).toFixed(2)}` - : undefined, - [amountOut, price], - ) - - const { - step, - executionDuration, - feesBreakdown, - gasFeesUSD, - protocolFeesUSD, - totalFeesUSD, - } = useMemo(() => { - const step = route.steps[0] - const executionDurationSeconds = step.estimate.executionDuration - const executionDurationMinutes = Math.floor(executionDurationSeconds / 60) - - const executionDuration = - executionDurationSeconds < 60 - ? `${executionDurationSeconds} seconds` - : `${executionDurationMinutes} minutes` - - const { feesBreakdown, totalFeesUSD, gasFeesUSD, protocolFeesUSD } = - getCrossChainFeesBreakdown(route.steps) - - return { - step, - executionDuration, - feesBreakdown, - totalFeesUSD, - gasFeesUSD, - protocolFeesUSD, - } - }, [route?.steps]) - - return ( - - - - {amountOut && token1 ? ( - `${amountOut?.toSignificant(6)} ${token1?.symbol}` - ) : ( -
- -
- )} -
- {`≈ ${amountOutUSD}`} -
- {isSelected ? ( - <> - - - {step ? ( -
- {step.includedSteps.map((_step, index) => { - return ( - - {_step.type === 'swap' ? ( - - ) : _step.type === 'cross' ? ( - - ) : null} - {index < step.includedSteps.length - 1 ? ( -
- - - - - -
- ) : null} -
- ) - })} -
- ) : ( - - )} -
- - - ) : null} - -
-
- - - {executionDuration} - - - - - {formatUSD(totalFeesUSD)} - - -
- - {step.toolDetails.name} - {step.toolDetails.name} - -
-
-
- ) -} 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 1967181b52..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 @@ -6,9 +6,9 @@ import { FC, ReactNode } from 'react' import { Chain } from 'sushi/chain' import { shortenAddress } from 'sushi/format' import { - UseCrossChainTradeRouteReturn, - useCrossChainSwapTrade, + UseSelectedCrossChainTradeRouteReturn, useDerivedStateCrossChainSwap, + useSelectedCrossChainTradeRoute, } from './derivedstate-cross-chain-swap-provider' interface ConfirmationDialogContent { @@ -16,7 +16,7 @@ interface ConfirmationDialogContent { dstTxHash?: string bridgeUrl?: string dialogState: { source: StepState; bridge: StepState; dest: StepState } - routeRef: React.MutableRefObject + routeRef: React.MutableRefObject } export const ConfirmationDialogContent: FC = ({ @@ -29,7 +29,7 @@ export const ConfirmationDialogContent: FC = ({ const { state: { chainId0, chainId1, token0, token1, recipient }, } = useDerivedStateCrossChainSwap() - const { data: trade } = useCrossChainSwapTrade() + const { data: trade } = useSelectedCrossChainTradeRoute() const swapOnDest = trade?.steps[0] && diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-token1-input.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-token1-input.tsx index 66ebf89b14..f91c26eebd 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-token1-input.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-token1-input.tsx @@ -4,8 +4,8 @@ import { XSWAP_SUPPORTED_CHAIN_IDS, getSortedChainIds } from 'src/config' import { Web3Input } from 'src/lib/wagmi/components/web3-input' import { isWNativeSupported } from 'sushi/config' import { - useCrossChainSwapTrade, useDerivedStateCrossChainSwap, + useSelectedCrossChainTradeRoute, } from './derivedstate-cross-chain-swap-provider' const networks = getSortedChainIds(XSWAP_SUPPORTED_CHAIN_IDS) @@ -18,10 +18,10 @@ export const CrossChainSwapToken1Input = () => { } = useDerivedStateCrossChainSwap() const { - isInitialLoading: isLoading, + isLoading, isFetching, - data: trade, - } = useCrossChainSwapTrade() + 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 470f95cbad..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 @@ -9,8 +9,8 @@ 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' @@ -19,15 +19,15 @@ export const CrossChainSwapTradeButton: FC = () => { const { state: { swapAmount, swapAmountString, chainId0 }, } = useDerivedStateCrossChainSwap() - const { data: trade, isError } = 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 ( @@ -43,20 +43,20 @@ export const CrossChainSwapTradeButton: FC = () => { id="approve-erc20" fullWidth amount={swapAmount} - contract={trade?.steps?.[0]?.estimate?.approvalAddress} + contract={route?.steps?.[0]?.estimate?.approvalAddress} >
- {warningSeverity(trade?.priceImpact) > 3 && ( + {warningSeverity(route?.priceImpact) > 3 && (
= ({ @@ -104,13 +104,13 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ const client0 = usePublicClient({ chainId: chainId0 }) const client1 = usePublicClient({ chainId: chainId1 }) const { approved } = useApproved(APPROVE_TAG_XSWAP) - const { data: trade } = useCrossChainSwapTrade() + const { data: selectedRoute } = useSelectedCrossChainTradeRoute() const { data: step, isFetching, isError: isStepQueryError, } = useCrossChainTradeStep({ - step: trade?.steps?.[0], + step: selectedRoute?.steps?.[0], query: { enabled: Boolean(approved && address), }, @@ -129,7 +129,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ dest: StepState.Success, }) - const routeRef = useRef(null) + const routeRef = useRef(null) const { data: estGas, @@ -317,8 +317,8 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ onSuccess: onWriteSuccess, onError: onWriteError, onMutate: () => { - if (routeRef && trade) { - routeRef.current = trade + if (routeRef && selectedRoute) { + routeRef.current = selectedRoute } }, }, diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-route.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-route.tsx index 0631f56836..9a86a1a0de 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-route.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-route.tsx @@ -4,15 +4,15 @@ import { classNames } from '@sushiswap/ui' import React from 'react' import { Chain } from 'sushi/chain' import { - useCrossChainSwapTrade, useDerivedStateCrossChainSwap, + useSelectedCrossChainTradeRoute, } from './derivedstate-cross-chain-swap-provider' export const CrossChainSwapTradeReviewRoute = () => { const { state: { chainId0 }, } = useDerivedStateCrossChainSwap() - const { data: trade } = useCrossChainSwapTrade() + const { data: trade } = useSelectedCrossChainTradeRoute() return (
{ @@ -25,7 +25,7 @@ export const CrossChainSwapTradeStats: FC = () => { const { state: { chainId0, chainId1, swapAmountString, recipient }, } = useDerivedStateCrossChainSwap() - const { isLoading, data: trade, isError } = useCrossChainSwapTrade() + const { isLoading, data: trade, isError } = useSelectedCrossChainTradeRoute() const feeData = useMemo( () => (trade?.steps ? getCrossChainFeesBreakdown(trade.steps) : undefined), diff --git a/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx b/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx index 27006927da..d47496750a 100644 --- a/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx +++ b/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx @@ -9,6 +9,7 @@ import { createContext, useCallback, useContext, + useEffect, useMemo, useState, } from 'react' @@ -16,7 +17,7 @@ import { isXSwapSupportedChainId } from 'src/config' import { useCrossChainTradeRoutes as _useCrossChainTradeRoutes } from 'src/lib/hooks/react-query' import { useSlippageTolerance } from 'src/lib/hooks/useSlippageTolerance' import { replaceNetworkSlug } from 'src/lib/network' -import { CrossChainRoute } from 'src/lib/swap/cross-chain' +import { CrossChainRoute, CrossChainRouteOrder } from 'src/lib/swap/cross-chain' import { useTokenWithCache } from 'src/lib/wagmi/hooks/tokens/useTokenWithCache' import { ChainId, ChainKey } from 'sushi/chain' import { defaultCurrency } from 'sushi/config' @@ -49,7 +50,8 @@ interface State { setSwapAmount(swapAmount: string): void switchTokens(): void setTradeId: Dispatch> - setRouteIndex(route: number): void + setSelectedBridge(bridge: string | undefined): void + setRouteOrder(order: CrossChainRouteOrder): void } state: { tradeId: string @@ -60,7 +62,8 @@ interface State { swapAmountString: string swapAmount: Amount | undefined recipient: Address | undefined - routeIndex: number + selectedBridge: string | undefined + routeOrder: CrossChainRouteOrder } isLoading: boolean isToken0Loading: boolean @@ -88,7 +91,10 @@ const DerivedstateCrossChainSwapProvider: FC< const searchParams = useSearchParams() const [tradeId, setTradeId] = useState(nanoid()) const [chainId, setChainId] = useState(defaultChainId) - const [routeIndex, setRouteIndex] = useState(0) + const [selectedBridge, setSelectedBridge] = useState( + undefined, + ) + const [routeOrder, setRouteOrder] = useState('CHEAPEST') const chainId0 = isXSwapSupportedChainId(chainId) ? chainId : ChainId.ETHEREUM @@ -312,7 +318,8 @@ const DerivedstateCrossChainSwapProvider: FC< setTradeId, switchTokens, setSwapAmount, - setRouteIndex, + setSelectedBridge, + setRouteOrder, }, state: { tradeId, @@ -323,7 +330,8 @@ const DerivedstateCrossChainSwapProvider: FC< swapAmount, token0: _token0, token1: _token1, - routeIndex, + selectedBridge, + routeOrder, }, isLoading: token0Loading || token1Loading, isToken0Loading: token0Loading, @@ -346,7 +354,8 @@ const DerivedstateCrossChainSwapProvider: FC< _token1, token1Loading, tradeId, - routeIndex, + selectedBridge, + routeOrder, ])} > {children} @@ -367,21 +376,35 @@ const useDerivedStateCrossChainSwap = () => { const useCrossChainTradeRoutes = () => { const { - state: { token1, swapAmount }, + state: { token1, swapAmount, selectedBridge, routeOrder }, + mutate: { setSelectedBridge }, } = useDerivedStateCrossChainSwap() const [slippagePercent] = useSlippageTolerance() const { address } = useAccount() - return _useCrossChainTradeRoutes({ + const query = _useCrossChainTradeRoutes({ fromAmount: swapAmount, toToken: token1, slippage: slippagePercent, fromAddress: address, + order: routeOrder, }) + + useEffect(() => { + if ( + query.data?.length && + (typeof selectedBridge === 'undefined' || + !query.data?.find((route) => route.steps[0].tool === selectedBridge)) + ) { + setSelectedBridge(query.data[0].steps[0].tool) + } + }, [query.data, selectedBridge, setSelectedBridge]) + + return query } -export interface UseCrossChainTradeRouteReturn extends CrossChainRoute { +export interface UseSelectedCrossChainTradeRouteReturn extends CrossChainRoute { tokenIn: Type tokenOut: Type amountIn?: Amount @@ -390,75 +413,72 @@ export interface UseCrossChainTradeRouteReturn extends CrossChainRoute { priceImpact?: Percent } -const useCrossChainSwapTrade = () => { +const useSelectedCrossChainTradeRoute = () => { const routesQuery = useCrossChainTradeRoutes() const { - state: { routeIndex }, + state: { selectedBridge }, } = useDerivedStateCrossChainSwap() - const route: UseCrossChainTradeRouteReturn | undefined = useMemo(() => { - if (!routesQuery.data?.[routeIndex]) return undefined + const route: UseSelectedCrossChainTradeRouteReturn | undefined = + useMemo(() => { + const route = routesQuery.data?.find( + (route) => route.steps[0].tool === selectedBridge, + ) - const route = routesQuery.data[routeIndex] + if (!route) return undefined - const tokenIn = - route.fromToken.address === zeroAddress - ? Native.onChain(route.fromToken.chainId) - : new Token(route.fromToken) + const tokenIn = + route.fromToken.address === zeroAddress + ? Native.onChain(route.fromToken.chainId) + : new Token(route.fromToken) - const tokenOut = - route.toToken.address === zeroAddress - ? Native.onChain(route.toToken.chainId) - : new Token(route.toToken) + const tokenOut = + route.toToken.address === zeroAddress + ? Native.onChain(route.toToken.chainId) + : new Token(route.toToken) - const amountIn = Amount.fromRawAmount(tokenIn, route.fromAmount) - const amountOut = Amount.fromRawAmount( - tokenOut, - routesQuery.data[routeIndex].toAmount, - ) - const amountOutMin = Amount.fromRawAmount( - tokenOut, - routesQuery.data[routeIndex].toAmountMin, - ) + const amountIn = Amount.fromRawAmount(tokenIn, route.fromAmount) + const amountOut = Amount.fromRawAmount(tokenOut, route.toAmount) + const amountOutMin = Amount.fromRawAmount(tokenOut, route.toAmountMin) - const fromAmountUSD = - (Number(route.fromToken.priceUSD) * Number(amountIn.quotient)) / - 10 ** tokenIn.decimals + const fromAmountUSD = + (Number(route.fromToken.priceUSD) * Number(amountIn.quotient)) / + 10 ** tokenIn.decimals - const toAmountUSD = - (Number(route.toToken.priceUSD) * Number(amountOut.quotient)) / - 10 ** tokenOut.decimals + const toAmountUSD = + (Number(route.toToken.priceUSD) * Number(amountOut.quotient)) / + 10 ** tokenOut.decimals - const priceImpact = new Percent( - Math.floor((fromAmountUSD / toAmountUSD - 1) * 10_000), - 10_000, - ) + const priceImpact = new Percent( + Math.floor((fromAmountUSD / toAmountUSD - 1) * 10_000), + 10_000, + ) - const gasSpent = Amount.fromRawAmount( - Native.onChain(route.fromChainId), - route.steps.reduce( - (total, step) => - total + - step.estimate.gasCosts.reduce( - (total, gasCost) => total + gasCost.amount, - 0n, - ), - 0n, - ), - ).toFixed(6) - - return { - ...route, - tokenIn, - tokenOut, - amountIn, - amountOut, - amountOutMin, - priceImpact, - gasSpent, - } - }, [routesQuery.data, routeIndex]) + // const gasSpent = Amount.fromRawAmount( + // Native.onChain(route.fromChainId), + // route.steps.reduce( + // (total, step) => + // total + + // step.estimate.gasCosts.reduce( + // (total, gasCost) => total + gasCost.amount, + // 0n, + // ), + // 0n, + // ), + // ).toFixed(6) + + return { + ...route, + tokenIn, + tokenOut, + amountIn, + amountOut, + amountOutMin, + priceImpact, + // gasSpent, + } + }, [routesQuery.data, selectedBridge]) return useMemo( () => ({ @@ -472,6 +492,6 @@ const useCrossChainSwapTrade = () => { export { DerivedstateCrossChainSwapProvider, useCrossChainTradeRoutes, - useCrossChainSwapTrade, + useSelectedCrossChainTradeRoute, useDerivedStateCrossChainSwap, } From e1dc8be9659c27582ca3a9dc38141fb4b23b0555 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Fri, 13 Dec 2024 22:28:46 +0700 Subject: [PATCH 03/26] feat: update xswap fee display format --- .../cross-chain-fees-hover-card.tsx | 112 ++++----- .../cross-chain-swap-trade-review-dialog.tsx | 215 ++++++++---------- 2 files changed, 155 insertions(+), 172 deletions(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx index 901124866b..cdd04781db 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx @@ -11,7 +11,7 @@ import { import { FC, useMemo } from 'react' import { FeesBreakdown } from 'src/lib/swap/cross-chain' import { ChainId } from 'sushi/chain' -import { formatUSD } from 'sushi/format' +import { formatNumber, formatUSD } from 'sushi/format' interface CrossChainFeesHoverCardProps { feesBreakdown: FeesBreakdown @@ -24,8 +24,6 @@ interface CrossChainFeesHoverCardProps { export const CrossChainFeesHoverCard: FC = ({ feesBreakdown, - gasFeesUSD, - protocolFeesUSD, chainId0, chainId1, children, @@ -33,75 +31,79 @@ export const CrossChainFeesHoverCard: FC = ({ const content = useMemo(() => { return ( - + {feesBreakdown.gas.size > 0 ? ( -
-
- Network Fees - {formatUSD(gasFeesUSD)} -
- {feesBreakdown.gas.get(chainId0) ? ( -
- Origin Chain +
+ Network Fees +
+ {feesBreakdown.gas.get(chainId0) ? ( - {feesBreakdown.gas.get(chainId0)!.amount.toSignificant(4)}{' '} - {feesBreakdown.gas.get(chainId0)!.amount.currency.symbol} + + {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) ? ( -
- Dest. Chain + ) : null} + {feesBreakdown.gas.get(chainId1) ? ( - {feesBreakdown.gas.get(chainId1)!.amount.toSignificant(4)}{' '} - {feesBreakdown.gas.get(chainId1)!.amount.currency.symbol} + + {formatNumber( + feesBreakdown.gas.get(chainId1)!.amount.toExact(), + )}{' '} + {feesBreakdown.gas.get(chainId1)!.amount.currency.symbol} + {' '} + ({formatUSD(feesBreakdown.gas.get(chainId1)!.amountUSD)}) -
- ) : null} + ) : null} +
) : null} {feesBreakdown.protocol.size > 0 ? ( -
-
- Protocol Fees - - {formatUSD(protocolFeesUSD)} - -
- {feesBreakdown.protocol.get(chainId0) ? ( -
- Origin Chain +
+ Protocol Fees +
+ {feesBreakdown.protocol.get(chainId0) ? ( - {feesBreakdown.protocol - .get(chainId0)! - .amount.toSignificant(4)}{' '} - { - feesBreakdown.protocol.get(chainId0)!.amount.currency - .symbol - } + + {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) ? ( -
- Dest. Chain + ) : null} + {feesBreakdown.protocol.get(chainId1) ? ( - {feesBreakdown.protocol - .get(chainId1)! - .amount.toSignificant(4)}{' '} - { - feesBreakdown.protocol.get(chainId1)!.amount.currency - .symbol - } + + {formatNumber( + feesBreakdown.protocol.get(chainId1)!.amount.toExact(), + )}{' '} + { + feesBreakdown.protocol.get(chainId1)!.amount.currency + .symbol + } + {' '} + ( + {formatUSD(feesBreakdown.protocol.get(chainId1)!.amountUSD)} + ) -
- ) : null} + ) : null} +
) : null} ) - }, [feesBreakdown, chainId0, chainId1, gasFeesUSD, protocolFeesUSD]) + }, [feesBreakdown, chainId0, chainId1]) return ( <> diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx index 77582385f5..3fbfee786e 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx @@ -53,7 +53,7 @@ import { warningSeverity } from 'src/lib/swap/warningSeverity' import { useApproved } from 'src/lib/wagmi/systems/Checker/Provider' import { Chain, ChainKey, chainName } from 'sushi/chain' import { Native } from 'sushi/currency' -import { formatUSD, shortenAddress } from 'sushi/format' +import { formatNumber, formatUSD, shortenAddress } from 'sushi/format' import { ZERO } from 'sushi/math' import { SendTransactionReturnType, @@ -586,125 +586,106 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ }` )} -
-
- Fees -
- {feeData ? ( -
- {feeData.feesBreakdown.gas.size > 0 ? ( -
- Network Fees - {feeData.feesBreakdown.gas.get(chainId0) ? ( - Origin Chain - ) : null} - {feeData.feesBreakdown.gas.get(chainId1) ? ( - Dest. Chain - ) : null} -
- ) : null} - {feeData.feesBreakdown.protocol.size > 0 ? ( -
- Protocol Fees - {feeData.feesBreakdown.protocol.get( - chainId0, - ) ? ( - Origin Chain - ) : null} - - {feeData.feesBreakdown.protocol.get( - chainId1, - ) ? ( - Dest. Chain - ) : null} -
- ) : null} -
+ {feeData && feeData.feesBreakdown.gas.size > 0 ? ( + +
+ {feeData.feesBreakdown.gas.get(chainId0) ? ( + + {formatNumber( + feeData.feesBreakdown.gas + .get(chainId0)! + .amount.toExact(), + )}{' '} + { + feeData.feesBreakdown.gas.get(chainId0)!.amount + .currency.symbol + }{' '} + + ( + {formatUSD( + feeData.feesBreakdown.gas.get(chainId0)! + .amountUSD, + )} + ) + + ) : null} -
-
-
- - {feeData && - feeData.totalFeesUSD !== feeData.gasFeesUSD && - feeData.totalFeesUSD !== feeData.protocolFeesUSD - ? formatUSD(feeData.totalFeesUSD) - : ''} - -
- {feeData ? ( -
- {feeData.feesBreakdown.gas.size > 0 ? ( -
- {formatUSD(feeData.gasFeesUSD)} - {feeData.feesBreakdown.gas.get(chainId0) ? ( - - {feeData.feesBreakdown.gas - .get(chainId0)! - .amount.toSignificant(4)}{' '} - { - feeData.feesBreakdown.gas.get(chainId0)! - .amount.currency.symbol - } - - ) : null} - {feeData.feesBreakdown.gas.get(chainId1) ? ( - - {feeData.feesBreakdown.gas - .get(chainId1)! - .amount.toSignificant(4)}{' '} - { - feeData.feesBreakdown.gas.get(chainId1)! - .amount.currency.symbol - } - - ) : null} -
- ) : null} - {feeData.feesBreakdown.protocol.size > 0 ? ( -
- - {formatUSD(feeData.protocolFeesUSD)} - - {feeData.feesBreakdown.protocol.get( - chainId0, - ) ? ( - - {feeData.feesBreakdown.protocol - .get(chainId0)! - .amount.toSignificant(4)}{' '} - { - feeData.feesBreakdown.protocol.get( - chainId0, - )!.amount.currency.symbol - } - - ) : null} - {feeData.feesBreakdown.protocol.get( - chainId1, - ) ? ( - - {feeData.feesBreakdown.protocol - .get(chainId1)! - .amount.toSignificant(4)}{' '} - { - feeData.feesBreakdown.protocol.get( - chainId1, - )!.amount.currency.symbol - } - - ) : null} -
- ) : null} -
+ {feeData.feesBreakdown.gas.get(chainId1) ? ( + + {formatNumber( + feeData.feesBreakdown.gas + .get(chainId1)! + .amount.toExact(), + )}{' '} + { + feeData.feesBreakdown.gas.get(chainId1)!.amount + .currency.symbol + }{' '} + + ( + {formatUSD( + feeData.feesBreakdown.gas.get(chainId1)! + .amountUSD, + )} + + ) + ) : null}
-
-
+ + ) : null} + {feeData && feeData.feesBreakdown.protocol.size > 0 ? ( + + {feeData ? ( +
+ {feeData.feesBreakdown.protocol.get(chainId0) ? ( + + {formatNumber( + feeData.feesBreakdown.protocol + .get(chainId0)! + .amount.toExact(), + )}{' '} + { + feeData.feesBreakdown.protocol.get(chainId0)! + .amount.currency.symbol + }{' '} + + ( + {formatUSD( + feeData.feesBreakdown.protocol.get( + chainId0, + )!.amountUSD, + )} + ) + + + ) : null} + {feeData.feesBreakdown.protocol.get(chainId1) ? ( + + {formatNumber( + feeData.feesBreakdown.protocol + .get(chainId1)! + .amount.toExact(), + )}{' '} + { + feeData.feesBreakdown.protocol.get(chainId1)! + .amount.currency.symbol + }{' '} + + ( + {formatUSD( + feeData.feesBreakdown.protocol.get( + chainId1, + )!.amountUSD, + )} + ) + + + ) : null} +
+ ) : null} +
+ ) : null} From a995cd2a7690a081b3b5db0bc33083a5f20991e8 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Sat, 14 Dec 2024 00:23:07 +0700 Subject: [PATCH 04/26] feat: add loading and no route states to CrossChainRouteSelector --- .../(trade)/cross-chain-swap/layout.tsx | 13 +- .../(trade)/cross-chain-swap/page.tsx | 25 +-- .../cross-chain-route-selector.tsx | 157 +++++++++++++----- apps/web/tailwind.config.js | 12 ++ 4 files changed, 146 insertions(+), 61 deletions(-) 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 7a1d91db48..4511eefc41 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,6 +1,7 @@ import { Metadata } from 'next' import { notFound } from 'next/navigation' import { XSWAP_SUPPORTED_CHAIN_IDS, isXSwapSupportedChainId } from 'src/config' +import { CrossChainRouteSelector } from 'src/ui/swap/cross-chain/cross-chain-route-selector' import { ChainId } from 'sushi/chain' import { SidebarContainer } from '~evm/_common/ui/sidebar' import { Providers } from './providers' @@ -27,8 +28,18 @@ export default function CrossChainSwapLayout({ selectedNetwork={chainId} supportedNetworks={XSWAP_SUPPORTED_CHAIN_IDS} unsupportedNetworkHref="/ethereum/cross-chain-swap" + shiftContent > -
{children}
+
+
+ {children} +
+
+ +
+
+
+
) 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 b9989552d8..9490de23d1 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,27 +1,10 @@ -'use client' - -import { Container, classNames } from '@sushiswap/ui' -import { CrossChainRouteSelector } from 'src/ui/swap/cross-chain/cross-chain-route-selector' +import { Container } from '@sushiswap/ui' 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 { data: routes, isLoading } = useCrossChainTradeRoutes() - const showRouteSelector = isLoading || (routes && routes.length > 0) - return ( -
- - - - {showRouteSelector ? ( - - ) : null} -
+ + + ) } diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx index 55ddae7165..4d8cd0b784 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx @@ -3,42 +3,38 @@ import { Card, CardContent, + CardFooter, CardHeader, CardTitle, - Collapsible, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, + cloudinaryFetchLoader, } from '@sushiswap/ui' -import { FC } from 'react' -import { CrossChainRoute as CrossChainRouteType } from 'src/lib/swap/cross-chain/types' +import Image from 'next/image' import { CrossChainRouteCard } from './cross-chain-route-card' -import { CrossChainRouteCardLoading } from './cross-chain-route-card-loading' -import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' +import { + useCrossChainTradeRoutes, + useDerivedStateCrossChainSwap, +} from './derivedstate-cross-chain-swap-provider' -interface CrossChainRouteSelectorProps { - routes: CrossChainRouteType[] | undefined - isLoading: boolean -} +export const CrossChainRouteSelector = () => { + const { data: routes, status } = useCrossChainTradeRoutes() -export const CrossChainRouteSelector: FC = ({ - routes, - isLoading, -}) => { const { state: { routeOrder, selectedBridge }, mutate: { setRouteOrder, setSelectedBridge }, } = useDerivedStateCrossChainSwap() return ( -
- - + + {status === 'success' && routes?.length > 0 ? ( + <>
Select A Route @@ -59,27 +55,110 @@ export const CrossChainRouteSelector: FC = ({ - {isLoading ? ( - <> - - {Array.from({ length: 3 }).map((_, index) => ( - - ))} - - ) : routes?.length ? ( - routes.map((route) => ( - setSelectedBridge(route.steps[0].tool)} + {/* {status === 'loading' ? ( + <> + + {Array.from({ length: 3 }).map((_, index) => ( + + ))} + + ) : routes?.length ? ( */} + {routes.map((route) => ( + setSelectedBridge(route.steps[0].tool)} + /> + ))} + {/* ) : null} */} + + + ) : status === 'error' || routes?.length === 0 ? ( + <> + + Select A Route + + + No Routes Found + + + No Routes Found. + + + ) : ( + <> + + + SushiXSwap Aggregator + + + +
+
+ sushi - )) - ) : null} + sushi + sushi + sushi + sushi + sushi + sushi +
+
- - -
+ + )} +
) } diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 5ceec1da3c..87d358aff3 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -16,6 +16,18 @@ const tailwindConfig = { 'stroke-dashoffset': '0', }, }, + carouselSlide: { + '0%': { transform: 'translateX(0%)' }, + '28.57%': { transform: 'translateX(0%)' }, + '33.33%': { transform: 'translateX(-50%)' }, + '61.90%': { transform: 'translateX(-50%)' }, + '66.66%': { transform: 'translateX(-100%)' }, + '95.23%': { transform: 'translateX(-100%)' }, + '100%': { transform: 'translateX(0%)' }, + }, + }, + animation: { + carouselSlide: 'carouselSlide 10.5s ease-in-out infinite', }, }, }, From 93c5a6be9677816955ccba8f9f05274027ce226f Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Thu, 19 Dec 2024 04:36:32 +0700 Subject: [PATCH 05/26] chore: update xswap layout --- .../[chainId]/(trade)/cross-chain-swap/layout.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) 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 4511eefc41..282b6b3a43 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 @@ -30,13 +30,11 @@ export default function CrossChainSwapLayout({ unsupportedNetworkHref="/ethereum/cross-chain-swap" shiftContent > -
-
- {children} -
-
- -
+
+ {children} +
+
+
From fcef60a758f0780a30d97f67ba7d45e786fe73f4 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Thu, 19 Dec 2024 04:45:36 +0700 Subject: [PATCH 06/26] chore: add back swap details to CrossChainRouteCard --- .../cross-chain/cross-chain-route-card.tsx | 152 +++++++++++++++++- 1 file changed, 144 insertions(+), 8 deletions(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx index a99cadbaeb..6df876d01d 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx @@ -1,22 +1,30 @@ -import { ClockIcon } from '@heroicons/react/24/outline' +import { ChevronDoubleRightIcon, ClockIcon } from '@heroicons/react/24/outline' +import { ArrowsRightLeftIcon } from '@heroicons/react/24/solid' import { Card, + CardContent, CardDescription, CardFooter, CardHeader, CardTitle, + Currency, + Separator, SkeletonText, classNames, } from '@sushiswap/ui' import { GasIcon } from '@sushiswap/ui/icons/GasIcon' import React, { FC, useMemo } from 'react' import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain' -import { +import type { + CrossChainAction as CrossChainActionType, CrossChainRoute as CrossChainRouteType, CrossChainRouteOrder, + CrossChainToolDetails as CrossChainToolDetailsType, } from 'src/lib/swap/cross-chain/types' -import { Amount } from 'sushi/currency' +import { Chain, ChainId } from 'sushi/chain' +import { Amount, Native, Token } from 'sushi/currency' import { formatUSD } from 'sushi/format' +import { zeroAddress } from 'viem' import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price' import { CrossChainFeesHoverCard } from './cross-chain-fees-hover-card' import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' @@ -134,10 +142,57 @@ export const CrossChainRouteCard: FC = ({
- -
-
- + {isSelected && step?.includedSteps.length > 1 ? ( + <> + + +
+ {step.includedSteps.map((_step, index) => { + return ( + + {_step.type === 'swap' ? ( + + ) : _step.type === 'cross' ? ( + + ) : null} + {index < step.includedSteps.length - 1 ? ( +
+ + + + + +
+ ) : null} +
+ ) + })} +
+
+ + + ) : null} + +
+
+ {executionDuration} @@ -162,10 +217,91 @@ export const CrossChainRouteCard: FC = ({ height={20} alt={step.toolDetails.name} /> - {step.toolDetails.name} + + {step.toolDetails.name} +
) } + +const SwapAction: FC<{ + action: CrossChainActionType + chainId0: ChainId +}> = ({ chainId0, action }) => { + const { fromToken, toToken, label, chain } = useMemo(() => { + const [label, chain] = + chainId0 === action.fromToken.chainId + ? [ + 'From', + Chain.fromChainId(action.fromToken.chainId)?.name?.toUpperCase(), + ] + : ['To', Chain.fromChainId(action.toToken.chainId)?.name?.toUpperCase()] + + return { + fromToken: + action.fromToken.address === zeroAddress + ? Native.onChain(action.fromToken.chainId) + : new Token(action.fromToken), + toToken: + action.toToken.address === zeroAddress + ? Native.onChain(action.toToken.chainId) + : new Token(action.toToken), + label, + chain, + } + }, [action.fromToken, action.toToken, chainId0]) + + return ( +
+ + {label}: {chain} + +
+
+ +
+
+ +
+
+ +
+
+ + Swap {fromToken.symbol} to {toToken.symbol} + +
+ ) +} + +const BridgeAction: FC<{ + action: CrossChainActionType + toolDetails: CrossChainToolDetailsType +}> = ({ action, toolDetails }) => { + return ( +
+ + Via{' '} + {toolDetails.name}{' '} + {toolDetails.name} + +
+
+ +
+
+ + Bridge {action.fromToken.symbol} + +
+ ) +} From 7c91072b09b6de9128c49c9f8f3916fe09183da9 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Thu, 19 Dec 2024 05:24:39 +0700 Subject: [PATCH 07/26] feat: display network name under token symbol --- .../web3-input/Currency/CurrencyInput.tsx | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) 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' )} From 618eb6cbcbbe02946d33d3106c8df8049daa30f0 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Thu, 19 Dec 2024 05:42:26 +0700 Subject: [PATCH 08/26] chore: add xswap review dialog fee subtitles --- .../cross-chain-swap-trade-review-dialog.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx index 3fbfee786e..0978b7909c 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx @@ -587,7 +587,10 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ )} {feeData && feeData.feesBreakdown.gas.size > 0 ? ( - +
{feeData.feesBreakdown.gas.get(chainId0) ? ( @@ -635,7 +638,10 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ ) : null} {feeData && feeData.feesBreakdown.protocol.size > 0 ? ( - + {feeData ? (
{feeData.feesBreakdown.protocol.get(chainId0) ? ( From 88a62929bf87002c8f97b96c5a2c15a8aa7b4005 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Fri, 20 Dec 2024 21:11:17 +0700 Subject: [PATCH 09/26] feat: xswap route visualization --- .../(trade)/cross-chain-swap/layout.tsx | 6 +- apps/web/src/lib/swap/cross-chain/types.ts | 4 + .../cross-chain-fees-hover-card.tsx | 130 ----- .../cross-chain-route-card-loading.tsx | 47 -- .../cross-chain-swap-fees-hover-card.tsx | 140 +++++ .../cross-chain-swap-proute-card-loading.tsx | 48 ++ ...rd.tsx => cross-chain-swap-route-card.tsx} | 148 +----- ...sx => cross-chain-swap-route-selector.tsx} | 6 +- .../cross-chain-swap-route-view.tsx | 199 +++++++ .../cross-chain-swap-trade-review-dialog.tsx | 501 ++++++++++++------ .../cross-chain-swap-trade-stats.tsx | 6 +- 11 files changed, 743 insertions(+), 492 deletions(-) delete mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx delete mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-route-card-loading.tsx create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-swap-fees-hover-card.tsx create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-swap-proute-card-loading.tsx rename apps/web/src/ui/swap/cross-chain/{cross-chain-route-card.tsx => cross-chain-swap-route-card.tsx} (50%) rename apps/web/src/ui/swap/cross-chain/{cross-chain-route-selector.tsx => cross-chain-swap-route-selector.tsx} (97%) create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx 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 282b6b3a43..6dd5739c8f 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,7 +1,7 @@ import { Metadata } from 'next' import { notFound } from 'next/navigation' import { XSWAP_SUPPORTED_CHAIN_IDS, isXSwapSupportedChainId } from 'src/config' -import { CrossChainRouteSelector } from 'src/ui/swap/cross-chain/cross-chain-route-selector' +import { CrossChainSwapRouteSelector } from 'src/ui/swap/cross-chain/cross-chain-swap-route-selector' import { ChainId } from 'sushi/chain' import { SidebarContainer } from '~evm/_common/ui/sidebar' import { Providers } from './providers' @@ -30,11 +30,11 @@ export default function CrossChainSwapLayout({ unsupportedNetworkHref="/ethereum/cross-chain-swap" shiftContent > -
+
{children}
- +
diff --git a/apps/web/src/lib/swap/cross-chain/types.ts b/apps/web/src/lib/swap/cross-chain/types.ts index 7cd70ce816..9111754dbb 100644 --- a/apps/web/src/lib/swap/cross-chain/types.ts +++ b/apps/web/src/lib/swap/cross-chain/types.ts @@ -1,6 +1,7 @@ import { z } from 'zod' import { crossChainActionSchema, + crossChainEstimateSchema, crossChainRouteSchema, crossChainStepSchema, crossChainToolDetailsSchema, @@ -9,6 +10,8 @@ import { type CrossChainAction = z.infer +type CrossChainEstimate = z.infer + type CrossChainRoute = z.infer type CrossChainStep = z.infer @@ -23,6 +26,7 @@ type CrossChainRouteOrder = 'CHEAPEST' | 'FASTEST' export type { CrossChainAction, + CrossChainEstimate, CrossChainRoute, CrossChainStep, CrossChainToolDetails, diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx deleted file mode 100644 index cdd04781db..0000000000 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-fees-hover-card.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { - Card, - CardContent, - HoverCard, - HoverCardContent, - HoverCardTrigger, - Popover, - PopoverContent, - PopoverTrigger, -} from '@sushiswap/ui' -import { FC, useMemo } from 'react' -import { FeesBreakdown } from 'src/lib/swap/cross-chain' -import { ChainId } from 'sushi/chain' -import { formatNumber, formatUSD } from 'sushi/format' - -interface CrossChainFeesHoverCardProps { - feesBreakdown: FeesBreakdown - gasFeesUSD: number - protocolFeesUSD: number - chainId0: ChainId - chainId1: ChainId - children: React.ReactNode -} - -export const CrossChainFeesHoverCard: FC = ({ - feesBreakdown, - chainId0, - chainId1, - children, -}) => { - const content = useMemo(() => { - return ( - - - {feesBreakdown.gas.size > 0 ? ( -
- Network Fees -
- {feesBreakdown.gas.get(chainId0) ? ( - - - {formatNumber( - feesBreakdown.gas.get(chainId0)!.amount.toExact(), - )}{' '} - {feesBreakdown.gas.get(chainId0)!.amount.currency.symbol} - {' '} - ({formatUSD(feesBreakdown.gas.get(chainId0)!.amountUSD)}) - - ) : null} - {feesBreakdown.gas.get(chainId1) ? ( - - - {formatNumber( - feesBreakdown.gas.get(chainId1)!.amount.toExact(), - )}{' '} - {feesBreakdown.gas.get(chainId1)!.amount.currency.symbol} - {' '} - ({formatUSD(feesBreakdown.gas.get(chainId1)!.amountUSD)}) - - ) : null} -
-
- ) : null} - {feesBreakdown.protocol.size > 0 ? ( -
- Protocol Fees -
- {feesBreakdown.protocol.get(chainId0) ? ( - - - {formatNumber( - feesBreakdown.protocol.get(chainId0)!.amount.toExact(), - )}{' '} - { - feesBreakdown.protocol.get(chainId0)!.amount.currency - .symbol - } - {' '} - ( - {formatUSD(feesBreakdown.protocol.get(chainId0)!.amountUSD)} - ) - - ) : null} - {feesBreakdown.protocol.get(chainId1) ? ( - - - {formatNumber( - feesBreakdown.protocol.get(chainId1)!.amount.toExact(), - )}{' '} - { - feesBreakdown.protocol.get(chainId1)!.amount.currency - .symbol - } - {' '} - ( - {formatUSD(feesBreakdown.protocol.get(chainId1)!.amountUSD)} - ) - - ) : null} -
-
- ) : null} -
-
- ) - }, [feesBreakdown, chainId0, chainId1]) - - return ( - <> -
- - {children} - - {content} - - -
-
- - e.stopPropagation()} asChild> - {children} - - - {content} - - -
- - ) -} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-card-loading.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-route-card-loading.tsx deleted file mode 100644 index bb85b58bf0..0000000000 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-route-card-loading.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - SkeletonText, -} from '@sushiswap/ui' -import { FC } from 'react' - -interface CrossChainRouteCardLoadingProps { - isSelected?: boolean -} - -export const CrossChainRouteCardLoading: FC = - () => { - return ( - - - -
- -
-
- -
- -
-
-
- -
-
- -
- -
- -
-
-
-
- ) - } diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-fees-hover-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-fees-hover-card.tsx new file mode 100644 index 0000000000..50bd6b4b20 --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-fees-hover-card.tsx @@ -0,0 +1,140 @@ +import { + Card, + CardContent, + HoverCard, + HoverCardContent, + HoverCardTrigger, + Popover, + PopoverContent, + PopoverTrigger, +} from '@sushiswap/ui' +import { FC, useMemo } from 'react' +import { FeesBreakdown } from 'src/lib/swap/cross-chain' +import { ChainId } from 'sushi/chain' +import { formatNumber, formatUSD } from 'sushi/format' + +interface CrossChainSwapFeesHoverCardProps { + feesBreakdown: FeesBreakdown + gasFeesUSD: number + protocolFeesUSD: number + chainId0: ChainId + chainId1: ChainId + children: React.ReactNode +} + +export const CrossChainSwapFeesHoverCard: FC = + ({ feesBreakdown, chainId0, chainId1, children }) => { + const content = useMemo(() => { + return ( + + + {feesBreakdown.gas.size > 0 ? ( +
+ Network Fees +
+ {feesBreakdown.gas.get(chainId0) ? ( + + + {formatNumber( + feesBreakdown.gas.get(chainId0)!.amount.toExact(), + )}{' '} + { + feesBreakdown.gas.get(chainId0)!.amount.currency + .symbol + } + {' '} + ({formatUSD(feesBreakdown.gas.get(chainId0)!.amountUSD)}) + + ) : null} + {feesBreakdown.gas.get(chainId1) ? ( + + + {formatNumber( + feesBreakdown.gas.get(chainId1)!.amount.toExact(), + )}{' '} + { + feesBreakdown.gas.get(chainId1)!.amount.currency + .symbol + } + {' '} + ({formatUSD(feesBreakdown.gas.get(chainId1)!.amountUSD)}) + + ) : null} +
+
+ ) : null} + {feesBreakdown.protocol.size > 0 ? ( +
+ Protocol Fees +
+ {feesBreakdown.protocol.get(chainId0) ? ( + + + {formatNumber( + feesBreakdown.protocol + .get(chainId0)! + .amount.toExact(), + )}{' '} + { + feesBreakdown.protocol.get(chainId0)!.amount.currency + .symbol + } + {' '} + ( + {formatUSD( + feesBreakdown.protocol.get(chainId0)!.amountUSD, + )} + ) + + ) : null} + {feesBreakdown.protocol.get(chainId1) ? ( + + + {formatNumber( + feesBreakdown.protocol + .get(chainId1)! + .amount.toExact(), + )}{' '} + { + feesBreakdown.protocol.get(chainId1)!.amount.currency + .symbol + } + {' '} + ( + {formatUSD( + feesBreakdown.protocol.get(chainId1)!.amountUSD, + )} + ) + + ) : null} +
+
+ ) : null} +
+
+ ) + }, [feesBreakdown, chainId0, chainId1]) + + return ( + <> +
+ + {children} + + {content} + + +
+
+ + e.stopPropagation()} asChild> + {children} + + + {content} + + +
+ + ) + } diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-proute-card-loading.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-proute-card-loading.tsx new file mode 100644 index 0000000000..d542d0ebe1 --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-proute-card-loading.tsx @@ -0,0 +1,48 @@ +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, + SkeletonText, +} from '@sushiswap/ui' +import { FC } from 'react' + +interface CrossChainSwapRouteCardLoadingProps { + isSelected?: boolean +} + +export const CrossChainSwapRouteCardLoading: FC< + CrossChainSwapRouteCardLoadingProps +> = () => { + return ( + + + +
+ +
+
+ +
+ +
+
+
+ +
+
+ +
+ +
+ +
+
+
+
+ ) +} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx similarity index 50% rename from apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx rename to apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx index 6df876d01d..5b64d68aca 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-route-card.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx @@ -1,5 +1,4 @@ -import { ChevronDoubleRightIcon, ClockIcon } from '@heroicons/react/24/outline' -import { ArrowsRightLeftIcon } from '@heroicons/react/24/solid' +import { ClockIcon } from '@heroicons/react/24/outline' import { Card, CardContent, @@ -7,7 +6,6 @@ import { CardFooter, CardHeader, CardTitle, - Currency, Separator, SkeletonText, classNames, @@ -16,27 +14,24 @@ import { GasIcon } from '@sushiswap/ui/icons/GasIcon' import React, { FC, useMemo } from 'react' import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain' import type { - CrossChainAction as CrossChainActionType, CrossChainRoute as CrossChainRouteType, CrossChainRouteOrder, - CrossChainToolDetails as CrossChainToolDetailsType, } from 'src/lib/swap/cross-chain/types' -import { Chain, ChainId } from 'sushi/chain' -import { Amount, Native, Token } from 'sushi/currency' +import { Amount } from 'sushi/currency' import { formatUSD } from 'sushi/format' -import { zeroAddress } from 'viem' import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price' -import { CrossChainFeesHoverCard } from './cross-chain-fees-hover-card' +import { CrossChainSwapFeesHoverCard } from './cross-chain-swap-fees-hover-card' +import { CrossChainSwapRouteView } from './cross-chain-swap-route-view' import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' -interface CrossChainRouteCardProps { +interface CrossChainSwapRouteCardProps { route: CrossChainRouteType order: CrossChainRouteOrder isSelected: boolean onSelect: () => void } -export const CrossChainRouteCard: FC = ({ +export const CrossChainSwapRouteCard: FC = ({ route, order, isSelected, @@ -142,51 +137,13 @@ export const CrossChainRouteCard: FC = ({
- {isSelected && step?.includedSteps.length > 1 ? ( + {isSelected && step.includedSteps.length > 1 ? ( <> - - -
- {step.includedSteps.map((_step, index) => { - return ( - - {_step.type === 'swap' ? ( - - ) : _step.type === 'cross' ? ( - - ) : null} - {index < step.includedSteps.length - 1 ? ( -
- - - - - -
- ) : null} -
- ) - })} -
+ + + - + ) : null} @@ -196,7 +153,7 @@ export const CrossChainRouteCard: FC = ({ {executionDuration}
- = ({ {formatUSD(totalFeesUSD)} - +
= ({ ) } - -const SwapAction: FC<{ - action: CrossChainActionType - chainId0: ChainId -}> = ({ chainId0, action }) => { - const { fromToken, toToken, label, chain } = useMemo(() => { - const [label, chain] = - chainId0 === action.fromToken.chainId - ? [ - 'From', - Chain.fromChainId(action.fromToken.chainId)?.name?.toUpperCase(), - ] - : ['To', Chain.fromChainId(action.toToken.chainId)?.name?.toUpperCase()] - - return { - fromToken: - action.fromToken.address === zeroAddress - ? Native.onChain(action.fromToken.chainId) - : new Token(action.fromToken), - toToken: - action.toToken.address === zeroAddress - ? Native.onChain(action.toToken.chainId) - : new Token(action.toToken), - label, - chain, - } - }, [action.fromToken, action.toToken, chainId0]) - - return ( -
- - {label}: {chain} - -
-
- -
-
- -
-
- -
-
- - Swap {fromToken.symbol} to {toToken.symbol} - -
- ) -} - -const BridgeAction: FC<{ - action: CrossChainActionType - toolDetails: CrossChainToolDetailsType -}> = ({ action, toolDetails }) => { - return ( -
- - Via{' '} - {toolDetails.name}{' '} - {toolDetails.name} - -
-
- -
-
- - Bridge {action.fromToken.symbol} - -
- ) -} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx similarity index 97% rename from apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx rename to apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx index 4d8cd0b784..cc05099569 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-route-selector.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx @@ -14,13 +14,13 @@ import { cloudinaryFetchLoader, } from '@sushiswap/ui' import Image from 'next/image' -import { CrossChainRouteCard } from './cross-chain-route-card' +import { CrossChainSwapRouteCard } from './cross-chain-swap-route-card' import { useCrossChainTradeRoutes, useDerivedStateCrossChainSwap, } from './derivedstate-cross-chain-swap-provider' -export const CrossChainRouteSelector = () => { +export const CrossChainSwapRouteSelector = () => { const { data: routes, status } = useCrossChainTradeRoutes() const { @@ -66,7 +66,7 @@ export const CrossChainRouteSelector = () => { ) : routes?.length ? ( */} {routes.map((route) => ( - = ({ + step, +}) => { + return step.includedSteps.length === 1 ? ( +
+
+ + Via{' '} + {step.toolDetails.name}{' '} + {step.toolDetails.name} + + + Bridge {step.action.fromToken.symbol} + +
+
+ ) : ( +
+ +
+ {step.includedSteps.map((_step) => { + return ( + + {_step.type === 'swap' ? ( + + ) : _step.type === 'cross' ? ( + + ) : null} + + ) + })} +
+
+ ) +} + +const VerticalDivider: FC<{ className?: string; count: number }> = ({ + className, + count, +}) => { + return ( +
+ {Array.from({ length: count }).map((_, i) => { + return i === 0 ? ( + + + + ) : ( + + + + ) + })} +
+ ) +} + +const SwapAction: FC<{ + action: CrossChainAction + estimate: CrossChainEstimate + chainId0: ChainId +}> = ({ chainId0, action, estimate }) => { + const { fromAmount, toAmount, label, chain, isWrap, isUnwrap } = + useMemo(() => { + const [label, chain] = + chainId0 === action.fromToken.chainId + ? [ + 'From', + Chain.fromChainId(action.fromToken.chainId)?.name?.toUpperCase(), + ] + : [ + 'To', + Chain.fromChainId(action.toToken.chainId)?.name?.toUpperCase(), + ] + + const fromToken = + action.fromToken.address === zeroAddress + ? Native.onChain(action.fromToken.chainId) + : new Token(action.fromToken) + + const toToken = + action.toToken.address === zeroAddress + ? Native.onChain(action.toToken.chainId) + : new Token(action.toToken) + + return { + fromAmount: Amount.fromRawAmount(fromToken, action.fromAmount), + toAmount: Amount.fromRawAmount(toToken, estimate.toAmount), + label, + chain, + isWrap: fromToken.isNative && fromToken.wrapped.equals(toToken), + isUnwrap: toToken.isNative && toToken.wrapped.equals(fromToken), + } + }, [ + action.fromToken, + action.toToken, + action.fromAmount, + estimate.toAmount, + chainId0, + ]) + + return ( +
+ + {label}{' '} + {' '} + {chain} + + + {isWrap ? ( + <>Wrap {toAmount.currency.symbol} + ) : isUnwrap ? ( + <>Unwrap {fromAmount.currency.symbol} + ) : ( + <> + Swap {formatNumber(fromAmount.toExact())}{' '} + {fromAmount.currency.symbol} + {' -> '} + {formatNumber(toAmount.toExact())} {toAmount.currency.symbol} + + )} + +
+ ) +} + +const BridgeAction: FC<{ + toolDetails: CrossChainToolDetails +}> = ({ toolDetails }) => { + return ( + + Via{' '} + {toolDetails.name}{' '} + {toolDetails.name} + + ) +} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx index 0978b7909c..b68a2f1cf5 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx @@ -25,6 +25,7 @@ import { DialogTitle, DialogType, Message, + SelectIcon, } from '@sushiswap/ui' import { Collapsible } from '@sushiswap/ui' import { Button } from '@sushiswap/ui' @@ -51,8 +52,8 @@ import { } from 'src/lib/swap/cross-chain' import { warningSeverity } from 'src/lib/swap/warningSeverity' import { useApproved } from 'src/lib/wagmi/systems/Checker/Provider' -import { Chain, ChainKey, chainName } from 'sushi/chain' -import { Native } from 'sushi/currency' +import { Chain, ChainKey } from 'sushi/chain' +import { Amount, Native } from 'sushi/currency' import { formatNumber, formatUSD, shortenAddress } from 'sushi/format' import { ZERO } from 'sushi/math' import { @@ -68,6 +69,7 @@ import { useTransaction, } from 'wagmi' import { useRefetchBalances } from '~evm/_common/ui/balance-provider/use-refetch-balances' +import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price' import { ConfirmationDialogContent, Divider, @@ -76,7 +78,7 @@ import { failedState, finishedState, } from './cross-chain-swap-confirmation-dialog' -import { CrossChainSwapTradeReviewRoute } from './cross-chain-swap-trade-review-route' +import { CrossChainSwapRouteView } from './cross-chain-swap-route-view' import { UseSelectedCrossChainTradeRouteReturn, useDerivedStateCrossChainSwap, @@ -86,6 +88,7 @@ import { export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ children, }) => { + const [showMore, setShowMore] = useState(false) const [slippagePercent] = useSlippageTolerance() const { address, chain } = useAccount() const { @@ -105,11 +108,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ const client1 = usePublicClient({ chainId: chainId1 }) const { approved } = useApproved(APPROVE_TAG_XSWAP) const { data: selectedRoute } = useSelectedCrossChainTradeRoute() - const { - data: step, - isFetching, - isError: isStepQueryError, - } = useCrossChainTradeStep({ + const { data: step, isError: isStepQueryError } = useCrossChainTradeStep({ step: selectedRoute?.steps?.[0], query: { enabled: Boolean(approved && address), @@ -468,9 +467,70 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ } }, [receipt?.hash]) - const feeData = useMemo( - () => (step ? getCrossChainFeesBreakdown([step]) : undefined), - [step], + 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 ( @@ -505,7 +565,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ - {isFetching ? ( + {!step?.amountOut ? ( ) : ( `Receive ${step?.amountOut?.toSignificant(6)} ${ @@ -520,185 +580,284 @@ 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 || !step?.priceImpact ? ( + + {!executionDuration ? ( ) : ( - `${ - step.priceImpact.lessThan(ZERO) - ? '+' - : step.priceImpact.greaterThan(ZERO) - ? '-' - : '' - }${Math.abs(Number(step.priceImpact.toFixed(2)))}%` - )} - - - {isFetching || !step?.amountOut ? ( - - ) : ( - `${step.amountOut.toSignificant(6)} ${token1?.symbol}` - )} - - - {isFetching || !step?.amountOutMin ? ( - - ) : ( - `${step.amountOutMin?.toSignificant(6)} ${ - token1?.symbol - }` + `${executionDuration}` )} - {feeData && feeData.feesBreakdown.gas.size > 0 ? ( - -
- {feeData.feesBreakdown.gas.get(chainId0) ? ( - - {formatNumber( - feeData.feesBreakdown.gas - .get(chainId0)! - .amount.toExact(), - )}{' '} - { - feeData.feesBreakdown.gas.get(chainId0)!.amount - .currency.symbol - }{' '} - - ( - {formatUSD( - feeData.feesBreakdown.gas.get(chainId0)! - .amountUSD, - )} - ) - - - ) : null} - {feeData.feesBreakdown.gas.get(chainId1) ? ( - - {formatNumber( - feeData.feesBreakdown.gas - .get(chainId1)! - .amount.toExact(), - )}{' '} - { - feeData.feesBreakdown.gas.get(chainId1)!.amount - .currency.symbol - }{' '} - - ( - {formatUSD( - feeData.feesBreakdown.gas.get(chainId1)! - .amountUSD, - )} - - ) - - ) : null} -
-
- ) : null} - {feeData && feeData.feesBreakdown.protocol.size > 0 ? ( - - {feeData ? ( -
- {feeData.feesBreakdown.protocol.get(chainId0) ? ( - - {formatNumber( - feeData.feesBreakdown.protocol - .get(chainId0)! - .amount.toExact(), - )}{' '} - { - feeData.feesBreakdown.protocol.get(chainId0)! - .amount.currency.symbol - }{' '} - - ( - {formatUSD( - feeData.feesBreakdown.protocol.get( - chainId0, - )!.amountUSD, - )} + {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)} - ) : null} - {feeData.feesBreakdown.protocol.get(chainId1) ? ( + )} +
+
+ + ) : ( + <> + + {!totalFeesUSD ? ( + + ) : ( +
- {formatNumber( - feeData.feesBreakdown.protocol - .get(chainId1)! - .amount.toExact(), - )}{' '} + {formatNumber(chainId0Fees)}{' '} { - feeData.feesBreakdown.protocol.get(chainId1)! - .amount.currency.symbol + feesBreakdown.gas.get(chainId0)!.amount + .currency.symbol }{' '} - ( - {formatUSD( - feeData.feesBreakdown.protocol.get( - chainId1, - )!.amountUSD, - )} - ) + ({formatUSD(totalFeesUSD)}) - ) : null} +
+ )} +
+ +
+ {!step?.amountOut ? ( + + ) : ( + {`${step.amountOut.toSignificant( + 6, + )} ${token1?.symbol}`} + )} + {!amountOutUSD ? ( + + ) : ( + + {formatUSD(amountOutUSD)} + + )}
- ) : null} -
- ) : null} - - - - - + + + )} + +
+ +
+ {step && ( + + + + + + )} {recipient && ( diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-stats.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-stats.tsx index aafec62564..a72c66da30 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-stats.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-stats.tsx @@ -14,7 +14,7 @@ import { warningSeverity, warningSeverityClassName, } from '../../../lib/swap/warningSeverity' -import { CrossChainFeesHoverCard } from './cross-chain-fees-hover-card' +import { CrossChainSwapFeesHoverCard } from './cross-chain-swap-fees-hover-card' import { useDerivedStateCrossChainSwap, useSelectedCrossChainTradeRoute, @@ -97,7 +97,7 @@ export const CrossChainSwapTradeStats: FC = () => { {isLoading || !feeData ? ( ) : ( - { {formatUSD(feeData.totalFeesUSD)} - + )}
From 62b66c504eb637a95115d0377619c33170549b27 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Sat, 21 Dec 2024 19:54:01 +0700 Subject: [PATCH 10/26] feat: lifi route selector mobile view --- .../(trade)/cross-chain-swap/layout.tsx | 7 +- .../cross-chain-swap-route-mobile-card.tsx | 261 ++++++++++++++++++ .../cross-chain-swap-route-selector.tsx | 117 +++++++- 3 files changed, 369 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx 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 6dd5739c8f..02ae3af57c 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,3 +1,4 @@ +import { Container } from '@sushiswap/ui' import { Metadata } from 'next' import { notFound } from 'next/navigation' import { XSWAP_SUPPORTED_CHAIN_IDS, isXSwapSupportedChainId } from 'src/config' @@ -30,13 +31,13 @@ export default function CrossChainSwapLayout({ unsupportedNetworkHref="/ethereum/cross-chain-swap" shiftContent > -
+
{children} -
+
-
+
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx new file mode 100644 index 0000000000..de7f86a7a4 --- /dev/null +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx @@ -0,0 +1,261 @@ +import { ClockIcon } from '@heroicons/react/24/outline' +import { Card, SkeletonCircle, SkeletonText, classNames } from '@sushiswap/ui' +import { GasIcon } from '@sushiswap/ui/icons/GasIcon' +import React, { FC, useMemo } from 'react' +import { getCrossChainFeesBreakdown } from 'src/lib/swap/cross-chain' +import type { + CrossChainRoute as CrossChainRouteType, + CrossChainRouteOrder, +} from 'src/lib/swap/cross-chain/types' +import { Amount } from 'sushi/currency' +import { formatUSD } from 'sushi/format' +import { usePrice } from '~evm/_common/ui/price-provider/price-provider/use-price' +import { CrossChainSwapFeesHoverCard } from './cross-chain-swap-fees-hover-card' +import { useDerivedStateCrossChainSwap } from './derivedstate-cross-chain-swap-provider' + +interface CrossChainSwapRouteMobileCardProps { + route: CrossChainRouteType | undefined + order: CrossChainRouteOrder + isSelected: boolean + onSelect?: () => void +} + +export const CrossChainSwapRouteMobileCard: FC< + CrossChainSwapRouteMobileCardProps +> = ({ route, order, isSelected, onSelect }) => { + const { + state: { token1, chainId0, chainId1 }, + } = useDerivedStateCrossChainSwap() + + const { data: price } = usePrice({ + chainId: token1?.chainId, + address: token1?.wrapped.address, + }) + + const amountOut = useMemo( + () => + route?.toAmount && token1 + ? Amount.fromRawAmount(token1, route.toAmount) + : undefined, + [token1, route?.toAmount], + ) + + const amountOutUSD = useMemo( + () => + price && amountOut + ? `${( + (price * Number(amountOut.quotient)) / + 10 ** amountOut.currency.decimals + ).toFixed(2)}` + : undefined, + [amountOut, price], + ) + + const { + step, + executionDuration, + feesBreakdown, + gasFeesUSD, + protocolFeesUSD, + totalFeesUSD, + } = useMemo(() => { + const step = route?.steps[0] + if (!step) + return { + step, + executionDuration: undefined, + feesBreakdown: undefined, + gasFeesUSD: undefined, + protocolFeesUSD: undefined, + totalFeesUSD: undefined, + } + + const executionDurationSeconds = step.estimate.executionDuration + const executionDurationMinutes = Math.floor(executionDurationSeconds / 60) + + const executionDuration = + executionDurationSeconds < 60 + ? `${executionDurationSeconds} seconds` + : `${executionDurationMinutes} minutes` + + const { feesBreakdown, totalFeesUSD, gasFeesUSD, protocolFeesUSD } = + getCrossChainFeesBreakdown(route.steps) + + return { + step, + executionDuration, + feesBreakdown, + totalFeesUSD, + gasFeesUSD, + protocolFeesUSD, + } + }, [route?.steps]) + + return ( + +
+ {step ? ( + {step.toolDetails.name} + ) : ( + + )} +
+
+ {amountOut && token1 ? ( + + {amountOut?.toSignificant(6)} {token1?.symbol} + + ) : ( +
+ +
+ )} + {amountOutUSD ? ( + + ≈ ${amountOutUSD} after fees + + ) : ( +
+ +
+ )} + {route?.tags?.includes(order) ? ( +
+ + {order === 'CHEAPEST' ? 'Best Return' : 'Fastest'} + +
+ ) : route?.tags?.includes( + order === 'FASTEST' ? 'CHEAPEST' : 'FASTEST', + ) ? ( +
+ + {order === 'FASTEST' ? 'Best Return' : 'Fastest'} + +
+ ) : null} +
+
+ {executionDuration ? ( + + + {executionDuration} + + ) : ( + + + + )} + {feesBreakdown ? ( + + + + {formatUSD(totalFeesUSD)} + + + ) : ( + + + + )} +
+
+
+ {/* +
+
+ + {amountOut && token1 ? ( + `${amountOut?.toSignificant(6)} ${token1?.symbol}` + ) : ( +
+ +
+ )} +
+ {`≈ ${amountOutUSD} after fees`} +
+ + {route.tags?.includes(order) ? ( +
+ + {order === 'CHEAPEST' ? 'Best Return' : 'Fastest'} + +
+ ) : route.tags?.includes( + order === 'FASTEST' ? 'CHEAPEST' : 'FASTEST', + ) ? ( +
+ + {order === 'FASTEST' ? 'Best Return' : 'Fastest'} + +
+ ) : null} +
+
+
+ {isSelected && step.includedSteps.length > 1 ? ( + <> + + + + + + + ) : null} + +
+
+ + + {executionDuration} + + + + + {formatUSD(totalFeesUSD)} + + +
+ + {step.toolDetails.name} + + {step.toolDetails.name} + + +
+
*/} +
+ ) +} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx index cc05099569..cd7d6f2046 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx @@ -1,20 +1,31 @@ 'use client' +import { ChevronRightIcon } from '@heroicons/react/24/outline' +import { useBreakpoint, useIsMounted, useMediaQuery } from '@sushiswap/hooks' import { Card, CardContent, CardFooter, CardHeader, CardTitle, + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, + ScrollArea, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, + SkeletonText, cloudinaryFetchLoader, } from '@sushiswap/ui' import Image from 'next/image' +import { useSidebar } from 'src/ui/sidebar' import { CrossChainSwapRouteCard } from './cross-chain-swap-route-card' +import { CrossChainSwapRouteMobileCard } from './cross-chain-swap-route-mobile-card' import { useCrossChainTradeRoutes, useDerivedStateCrossChainSwap, @@ -22,16 +33,25 @@ import { export const CrossChainSwapRouteSelector = () => { const { data: routes, status } = useCrossChainTradeRoutes() + const { isOpen: isSidebarOpen } = useSidebar() + const isMounted = useIsMounted() const { state: { routeOrder, selectedBridge }, mutate: { setRouteOrder, setSelectedBridge }, } = useDerivedStateCrossChainSwap() - return ( + const isLg = useMediaQuery({ + query: `(min-width: 1056px)`, + }) + const { isXl } = useBreakpoint('xl') + + const showDesktopSelector = isSidebarOpen ? isXl : isLg + + return !isMounted ? null : showDesktopSelector ? ( {status === 'success' && routes?.length > 0 ? ( <> @@ -55,16 +75,6 @@ export const CrossChainSwapRouteSelector = () => { - {/* {status === 'loading' ? ( - <> - - {Array.from({ length: 3 }).map((_, index) => ( - - ))} - - ) : routes?.length ? ( */} {routes.map((route) => ( { onSelect={() => setSelectedBridge(route.steps[0].tool)} /> ))} - {/* ) : null} */} ) : status === 'error' || routes?.length === 0 ? ( @@ -111,6 +120,7 @@ export const CrossChainSwapRouteSelector = () => { alt="sushi" width={95} height={80} + quality={100} /> { alt="sushi" width={95} height={80} + quality={100} /> { alt="sushi" width={95} height={80} + quality={100} /> { alt="sushi" width={95} height={80} + quality={100} /> { alt="sushi" width={95} height={80} + quality={100} /> { alt="sushi" width={95} height={80} + quality={100} /> { alt="sushi" width={95} height={80} + quality={100} />
@@ -160,5 +176,80 @@ export const CrossChainSwapRouteSelector = () => { )} + ) : ( + + + + Select A Route +
+ +
+
+ + +
+ {routes?.map((route) => ( + setSelectedBridge(route.steps[0].tool)} + /> + ))} +
+
+
+ + + +
+ + Selected Route + + {routes && routes.length > 0 ? ( + + + View All ({routes?.length}) + + + + ) : status === 'pending' ? ( + + + + ) : null} +
+
+ + {status === 'error' || routes?.length === 0 ? ( +
+ No Routes Found. +
+ ) : ( + route.steps[0].tool === selectedBridge, + )} + order={routeOrder} + isSelected={true} + /> + )} +
+
+
) } From 4713f04a5a5668ceaa13cc51e76c15ece96785fa Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Tue, 24 Dec 2024 15:20:27 +0700 Subject: [PATCH 11/26] chore: style --- .../cross-chain-swap-proute-card-loading.tsx | 48 ---- .../cross-chain-swap-route-card.tsx | 4 +- .../cross-chain-swap-route-mobile-card.tsx | 80 +----- .../cross-chain-swap-route-selector.tsx | 268 +++++++++++------- .../cross-chain-swap-route-view.tsx | 9 +- 5 files changed, 182 insertions(+), 227 deletions(-) delete mode 100644 apps/web/src/ui/swap/cross-chain/cross-chain-swap-proute-card-loading.tsx diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-proute-card-loading.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-proute-card-loading.tsx deleted file mode 100644 index d542d0ebe1..0000000000 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-proute-card-loading.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { - Card, - CardDescription, - CardFooter, - CardHeader, - CardTitle, - SkeletonText, -} from '@sushiswap/ui' -import { FC } from 'react' - -interface CrossChainSwapRouteCardLoadingProps { - isSelected?: boolean -} - -export const CrossChainSwapRouteCardLoading: FC< - CrossChainSwapRouteCardLoadingProps -> = () => { - return ( - - - -
- -
-
- -
- -
-
-
- -
-
- -
- -
- -
-
-
-
- ) -} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx index 5b64d68aca..7b9e007400 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx @@ -121,7 +121,7 @@ export const CrossChainSwapRouteCard: FC = ({ {route.tags?.includes(order) ? (
- + {order === 'CHEAPEST' ? 'Best Return' : 'Fastest'}
@@ -129,7 +129,7 @@ export const CrossChainSwapRouteCard: FC = ({ order === 'FASTEST' ? 'CHEAPEST' : 'FASTEST', ) ? (
- + {order === 'FASTEST' ? 'Best Return' : 'Fastest'}
diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx index de7f86a7a4..429ae5d56f 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-mobile-card.tsx @@ -134,7 +134,7 @@ export const CrossChainSwapRouteMobileCard: FC< )} {route?.tags?.includes(order) ? (
- + {order === 'CHEAPEST' ? 'Best Return' : 'Fastest'}
@@ -142,7 +142,7 @@ export const CrossChainSwapRouteMobileCard: FC< order === 'FASTEST' ? 'CHEAPEST' : 'FASTEST', ) ? (
- + {order === 'FASTEST' ? 'Best Return' : 'Fastest'}
@@ -180,82 +180,6 @@ export const CrossChainSwapRouteMobileCard: FC<
- {/* -
-
- - {amountOut && token1 ? ( - `${amountOut?.toSignificant(6)} ${token1?.symbol}` - ) : ( -
- -
- )} -
- {`≈ ${amountOutUSD} after fees`} -
- - {route.tags?.includes(order) ? ( -
- - {order === 'CHEAPEST' ? 'Best Return' : 'Fastest'} - -
- ) : route.tags?.includes( - order === 'FASTEST' ? 'CHEAPEST' : 'FASTEST', - ) ? ( -
- - {order === 'FASTEST' ? 'Best Return' : 'Fastest'} - -
- ) : null} -
-
-
- {isSelected && step.includedSteps.length > 1 ? ( - <> - - - - - - - ) : null} - -
-
- - - {executionDuration} - - - - - {formatUSD(totalFeesUSD)} - - -
- - {step.toolDetails.name} - - {step.toolDetails.name} - - -
-
*/} ) } diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx index cd7d6f2046..b948aa5b57 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx @@ -19,10 +19,12 @@ import { SelectItem, SelectTrigger, SelectValue, - SkeletonText, cloudinaryFetchLoader, } from '@sushiswap/ui' +import { QueryStatus } from '@tanstack/react-query' import Image from 'next/image' +import { FC } from 'react' +import { CrossChainRoute, CrossChainRouteOrder } from 'src/lib/swap/cross-chain' import { useSidebar } from 'src/ui/sidebar' import { CrossChainSwapRouteCard } from './cross-chain-swap-route-card' import { CrossChainSwapRouteMobileCard } from './cross-chain-swap-route-mobile-card' @@ -49,11 +51,49 @@ export const CrossChainSwapRouteSelector = () => { const showDesktopSelector = isSidebarOpen ? isXl : isLg return !isMounted ? null : showDesktopSelector ? ( + + ) : ( + + ) +} + +interface RouteSelectorProps { + routeOrder: CrossChainRouteOrder + setRouteOrder: (order: CrossChainRouteOrder) => void + selectedBridge: string | undefined + setSelectedBridge: (bridge: string | undefined) => void + routes: CrossChainRoute[] | undefined + status: QueryStatus +} + +const DesktopRouteSelector: FC = ({ + routeOrder, + setRouteOrder, + routes, + selectedBridge, + setSelectedBridge, + status, +}) => { + return ( - {status === 'success' && routes?.length > 0 ? ( + {status === 'success' && routes && routes.length > 0 ? ( <>
@@ -98,6 +138,7 @@ export const CrossChainSwapRouteSelector = () => { alt="No Routes Found" width={506} height={284} + quality={100} /> @@ -112,71 +153,23 @@ export const CrossChainSwapRouteSelector = () => { -
-
- sushi - sushi - sushi - sushi - sushi - sushi - sushi -
-
+
)} - ) : ( + ) +} + +const MobileRouteSelector: FC = ({ + routeOrder, + setRouteOrder, + routes, + selectedBridge, + setSelectedBridge, + status, +}) => { + return ( @@ -215,41 +208,122 @@ export const CrossChainSwapRouteSelector = () => { variant="outline" className="w-full bg-gray-50 dark:bg-slate-800 overflow-hidden flex flex-col" > - -
- - Selected Route - - {routes && routes.length > 0 ? ( - - - View All ({routes?.length}) - - - - ) : status === 'pending' ? ( - - - - ) : null} -
-
- - {status === 'error' || routes?.length === 0 ? ( -
- No Routes Found. -
- ) : ( - route.steps[0].tool === selectedBridge, - )} - order={routeOrder} - isSelected={true} - /> - )} -
+ {status === 'success' && routes && routes.length > 0 ? ( + <> + +
+ + Selected Route + + + + View All ({routes.length}) + + + +
+
+ + route.steps[0].tool === selectedBridge, + )} + order={routeOrder} + isSelected={true} + /> + + + ) : status === 'error' || routes?.length === 0 ? ( + <> + + + Selected Route + + + +
+ No Routes Found. +
+
+ + ) : ( + <> + + + SushiXSwap Aggregator + + + + + + + )}
) } + +const RouteSelectorCarousel = () => { + return ( +
+
+ sushi + sushi + sushi + sushi + sushi + sushi + sushi +
+
+ ) +} diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx index bb8e0b186d..ad0b00686d 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx @@ -162,8 +162,13 @@ const SwapAction: FC<{ />{' '} {chain} - - {isWrap ? ( + + {label === 'To' ? ( + <> + Receive {formatNumber(toAmount.toExact())}{' '} + {toAmount.currency.symbol} + + ) : isWrap ? ( <>Wrap {toAmount.currency.symbol} ) : isUnwrap ? ( <>Unwrap {fromAmount.currency.symbol} From 991c79371cfbf736ed5d767ee71ee648870ae7b7 Mon Sep 17 00:00:00 2001 From: Daniel Reinoso Date: Mon, 6 Jan 2025 12:25:38 -0300 Subject: [PATCH 12/26] feat: Add toggle to collect fees as wrapped or native --- .../ConcentratedLiquidityCollectButton.tsx | 10 +- .../ConcentratedLiquidityCollectWidget.tsx | 116 ++++++++++++++++++ apps/web/src/ui/pool/V3PositionView.tsx | 55 ++------- 3 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx index b64a66324d..1dc856ef26 100644 --- a/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx +++ b/apps/web/src/ui/pool/ConcentratedLiquidityCollectButton.tsx @@ -13,7 +13,7 @@ import { SUSHISWAP_V3_POSTIION_MANAGER, isSushiSwapV3ChainId, } from 'sushi/config' -import { Amount, Type, unwrapToken } from 'sushi/currency' +import { Amount, Type } from 'sushi/currency' import { NonfungiblePositionManager, Position } from 'sushi/pool/sushiswap-v3' import { Hex, SendTransactionReturnType, UserRejectedRequestError } from 'viem' import { @@ -69,16 +69,14 @@ export const ConcentratedLiquidityCollectButton: FC< ? Amount.fromRawAmount(token0, positionDetails.fees[0]) : undefined const feeValue1 = positionDetails.fees - ? Amount.fromRawAmount(token0, positionDetails.fees[1]) + ? Amount.fromRawAmount(token1, positionDetails.fees[1]) : undefined const { calldata, value } = NonfungiblePositionManager.collectCallParameters({ tokenId: positionDetails.tokenId.toString(), - expectedCurrencyOwed0: - feeValue0 ?? Amount.fromRawAmount(unwrapToken(token0), 0), - expectedCurrencyOwed1: - feeValue1 ?? Amount.fromRawAmount(unwrapToken(token1), 0), + expectedCurrencyOwed0: feeValue0 ?? Amount.fromRawAmount(token0, 0), + expectedCurrencyOwed1: feeValue1 ?? Amount.fromRawAmount(token1, 0), recipient: account, }) diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx new file mode 100644 index 0000000000..6e295c45a9 --- /dev/null +++ b/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx @@ -0,0 +1,116 @@ +'use client' + +import { + CardContent, + CardCurrencyAmountItem, + CardFooter, + CardGroup, + CardLabel, + Switch, +} from '@sushiswap/ui' +import { Button } from '@sushiswap/ui' +import { FC, useMemo, useState } from 'react' +import { ConcentratedLiquidityPosition } from 'src/lib/wagmi/hooks/positions/types' +import { Checker } from 'src/lib/wagmi/systems/Checker' +import { Address, ChainId, Position } from 'sushi' +import { Amount, Native, Type, unwrapToken } from 'sushi/currency' +import { formatUSD } from 'sushi/format' +import { ConcentratedLiquidityCollectButton } from './ConcentratedLiquidityCollectButton' + +interface ConcentratedLiquidityCollectWidget { + position: Position | undefined + positionDetails: ConcentratedLiquidityPosition | undefined + token0: Type | undefined + token1: Type | undefined + chainId: ChainId + isLoading: boolean + address: Address | undefined + amounts: undefined[] | Amount[] + fiatValuesAmounts: number[] +} + +export const ConcentratedLiquidityCollectWidget: FC< + ConcentratedLiquidityCollectWidget +> = ({ + position, + positionDetails, + token0, + token1, + chainId, + isLoading, + address, + amounts, + fiatValuesAmounts, +}) => { + const [receiveWrapped, setReceiveWrapped] = useState(false) + const nativeToken = useMemo(() => Native.onChain(chainId), [chainId]) + + const positionHasNativeToken = useMemo(() => { + if (!nativeToken || !token0 || !token1) return false + return ( + token0.isNative || + token1.isNative || + token0.address === nativeToken?.wrapped?.address || + token1.address === nativeToken?.wrapped?.address + ) + }, [token0, token1, nativeToken]) + + const expectedToken0 = useMemo(() => { + return !token0 || receiveWrapped ? token0?.wrapped : unwrapToken(token0) + }, [token0, receiveWrapped]) + + const expectedToken1 = useMemo(() => { + return !token1 || receiveWrapped ? token1?.wrapped : unwrapToken(token1) + }, [token1, receiveWrapped]) + + return ( + <> + + + Tokens + + + + {positionHasNativeToken ? ( +
+ + {`Receive ${nativeToken.wrapped.symbol} as ${nativeToken.symbol}`} + + +
+ ) : null} +
+ + + {({ send, isPending }) => ( + + + + + + )} + + + + ) +} diff --git a/apps/web/src/ui/pool/V3PositionView.tsx b/apps/web/src/ui/pool/V3PositionView.tsx index 4deb0b703b..73a6767f57 100644 --- a/apps/web/src/ui/pool/V3PositionView.tsx +++ b/apps/web/src/ui/pool/V3PositionView.tsx @@ -53,7 +53,7 @@ import { } from '../../lib/functions' import { usePriceInverter, useTokenAmountDollarValues } from '../../lib/hooks' import { useIsTickAtLimit } from '../../lib/pool/v3' -import { ConcentratedLiquidityCollectButton } from './ConcentratedLiquidityCollectButton' +import { ConcentratedLiquidityCollectWidget } from './ConcentratedLiquidityCollectWidget' import { ConcentratedLiquidityHarvestButton } from './ConcentratedLiquidityHarvestButton' import { ConcentratedLiquidityProvider, @@ -296,46 +296,17 @@ const Component: FC<{ chainId: string; address: string; position: string }> = ({ {formatUSD(fiatValuesAmounts[0] + fiatValuesAmounts[1])} - - - Tokens - - - - - - - {({ send, isPending }) => ( - - - - - - )} - - + {isMerklChainId(chainId) ? ( @@ -512,7 +483,7 @@ const Component: FC<{ chainId: string; address: string; position: string }> = ({ {unwrapToken(currencyQuote)?.symbol}{' '} - + ( {formatPercent( priceLower From 00de12715999ea394a49a02790117ae0e7282435 Mon Sep 17 00:00:00 2001 From: Daniel Reinoso Date: Mon, 6 Jan 2025 12:28:37 -0300 Subject: [PATCH 13/26] feat: Add toggle to remove liquidity as wrapped or native --- .../ConcentratedLiquidityRemoveWidget.tsx | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx index ae4ea11ac4..867e333404 100644 --- a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx +++ b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx @@ -35,6 +35,7 @@ import { List, SettingsModule, SettingsOverlay, + Switch, classNames, } from '@sushiswap/ui' import { Button } from '@sushiswap/ui' @@ -52,7 +53,7 @@ import { SushiSwapV3ChainId, isSushiSwapV3ChainId, } from 'sushi/config' -import { Amount, Type, unwrapToken } from 'sushi/currency' +import { Amount, Native, Type, unwrapToken } from 'sushi/currency' import { Percent, ZERO } from 'sushi/math' import { NonfungiblePositionManager, Position } from 'sushi/pool/sushiswap-v3' import { Hex, SendTransactionReturnType, UserRejectedRequestError } from 'viem' @@ -90,6 +91,7 @@ export const ConcentratedLiquidityRemoveWidget: FC< const { chain } = useAccount() const client = usePublicClient() const [value, setValue] = useState('0') + const [receiveWrapped, setReceiveWrapped] = useState(false) const [slippageTolerance] = useSlippageTolerance( SlippageToleranceStorageKey.RemoveLiquidity, ) @@ -163,18 +165,34 @@ export const ConcentratedLiquidityRemoveWidget: FC< const [feeValue0, feeValue1] = useMemo(() => { if (positionDetails && token0 && token1) { + const expectedToken0 = + !token0 || receiveWrapped ? token0?.wrapped : unwrapToken(token0) + const expectedToken1 = + !token1 || receiveWrapped ? token1?.wrapped : unwrapToken(token1) const feeValue0 = positionDetails.fees - ? Amount.fromRawAmount(token0, positionDetails.fees[0]) + ? Amount.fromRawAmount(expectedToken0, positionDetails.fees[0]) : undefined const feeValue1 = positionDetails.fees - ? Amount.fromRawAmount(token1, positionDetails.fees[1]) + ? Amount.fromRawAmount(expectedToken1, positionDetails.fees[1]) : undefined return [feeValue0, feeValue1] } return [undefined, undefined] - }, [positionDetails, token0, token1]) + }, [positionDetails, token0, token1, receiveWrapped]) + + const nativeToken = useMemo(() => Native.onChain(chainId), [chainId]) + + const positionHasNativeToken = useMemo(() => { + if (!nativeToken || !token0 || !token1) return false + return ( + token0.isNative || + token1.isNative || + token0.address === nativeToken?.wrapped?.address || + token1.address === nativeToken?.wrapped?.address + ) + }, [token0, token1, nativeToken]) const prepare = useMemo(() => { const liquidityPercentage = new Percent(debouncedValue, 100) @@ -185,18 +203,23 @@ export const ConcentratedLiquidityRemoveWidget: FC< ? liquidityPercentage.multiply(position.amount1.quotient).quotient : undefined + const expectedToken0 = + receiveWrapped && token0 ? unwrapToken(token0) : token0 + const expectedToken1 = + receiveWrapped && token1 ? unwrapToken(token1) : token1 + const liquidityValue0 = - token0 && typeof discountedAmount0 === 'bigint' - ? Amount.fromRawAmount(unwrapToken(token0), discountedAmount0) + expectedToken0 && typeof discountedAmount0 === 'bigint' + ? Amount.fromRawAmount(expectedToken0, discountedAmount0) : undefined const liquidityValue1 = - token1 && typeof discountedAmount1 === 'bigint' - ? Amount.fromRawAmount(unwrapToken(token1), discountedAmount1) + expectedToken1 && typeof discountedAmount1 === 'bigint' + ? Amount.fromRawAmount(expectedToken1, discountedAmount1) : undefined if ( - token0 && - token1 && + expectedToken0 && + expectedToken1 && position && account && positionDetails && @@ -253,6 +276,7 @@ export const ConcentratedLiquidityRemoveWidget: FC< token0, token1, debouncedValue, + receiveWrapped, ]) const { isError: isSimulationError } = useCall({ @@ -566,6 +590,17 @@ export const ConcentratedLiquidityRemoveWidget: FC< )} + {positionHasNativeToken ? ( +
+ + {`Receive ${nativeToken.wrapped.symbol} as ${nativeToken.symbol}`} + + +
+ ) : null}
- -
- {routes?.map((route) => ( - setSelectedBridge(route.steps[0].tool)} - /> - ))} -
-
+
+ {routes?.map((route) => ( + setSelectedBridge(route.steps[0].tool)} + /> + ))} +
Date: Thu, 9 Jan 2025 16:21:00 +0700 Subject: [PATCH 16/26] chore: add cronos to XSWAP_SUPPORTED_CHAIN_IDS --- apps/web/src/config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/config.ts b/apps/web/src/config.ts index 699b37a7a8..1c565ee5b4 100644 --- a/apps/web/src/config.ts +++ b/apps/web/src/config.ts @@ -240,6 +240,7 @@ export const XSWAP_SUPPORTED_CHAIN_IDS = [ ChainId.BLAST, ChainId.BOBA, ChainId.CELO, + ChainId.CRONOS, ChainId.ETHEREUM, ChainId.FUSE, ChainId.FANTOM, From 223c84182e2cd7f5a03855867de37468de2dc2ce Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Fri, 10 Jan 2025 16:50:33 +0700 Subject: [PATCH 17/26] chore: add copy to xswap route selector loading state --- .../cross-chain-swap-route-selector.tsx | 39 ++++++++++++++++--- apps/web/tailwind.config.js | 2 +- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx index 1017da8723..7438d7c794 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx @@ -5,6 +5,7 @@ import { useBreakpoint, useMediaQuery } from '@sushiswap/hooks' import { Card, CardContent, + CardDescription, CardFooter, CardHeader, CardTitle, @@ -22,7 +23,7 @@ import { } from '@sushiswap/ui' import { QueryStatus } from '@tanstack/react-query' import Image from 'next/image' -import { FC } from 'react' +import { FC, useState } from 'react' import { CrossChainRoute, CrossChainRouteOrder } from 'src/lib/swap/cross-chain' import { useSidebar } from 'src/ui/sidebar' import { CrossChainSwapRouteCard } from './cross-chain-swap-route-card' @@ -149,10 +150,20 @@ const DesktopRouteSelector: FC = ({ SushiXSwap Aggregator + + Swap Anything On Any Chain + + +
+ ✨ Best Pricing + 🍣 Fastest Response Time +
+ ‍‍👨‍🍳 Widest Network Coverage +
)}
@@ -167,13 +178,21 @@ const MobileRouteSelector: FC = ({ setSelectedBridge, status, }) => { + const [dialogOpen, setDialogOpen] = useState(false) + return ( - + Select A Route
- { + setRouteOrder(order) + setDialogOpen(false) + }} + > Sort By: @@ -244,14 +263,24 @@ const MobileRouteSelector: FC = ({ ) : ( <> - - + + SushiXSwap Aggregator + + Swap Anything On Any Chain + + +
+ ✨ Best Pricing + 🍣 Fastest Response Time +
+ ‍‍👨‍🍳 Widest Network Coverage +
)} diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index 87d358aff3..cf5388ea2e 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -27,7 +27,7 @@ const tailwindConfig = { }, }, animation: { - carouselSlide: 'carouselSlide 10.5s ease-in-out infinite', + carouselSlide: 'carouselSlide 6s ease-in-out infinite', }, }, }, From b3a27911e941d772ef4446d9dfc2ce75c1b22b76 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Fri, 10 Jan 2025 17:54:58 +0700 Subject: [PATCH 18/26] fix: xswap route view --- .../cross-chain-swap-route-view.tsx | 197 ++++++++++-------- 1 file changed, 113 insertions(+), 84 deletions(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx index ad0b00686d..093bf65995 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-view.tsx @@ -8,8 +8,8 @@ import type { CrossChainStep, CrossChainToolDetails, } from 'src/lib/swap/cross-chain/types' -import { Chain, ChainId } from 'sushi/chain' -import { Amount, Native, Token } from 'sushi/currency' +import { Chain } from 'sushi/chain' +import { Amount, Native, Token, Type } from 'sushi/currency' import { formatNumber } from 'sushi/format' import { zeroAddress } from 'viem' @@ -20,47 +20,64 @@ interface CrossChainSwapRouteViewProps { export const CrossChainSwapRouteView: FC = ({ step, }) => { - return step.includedSteps.length === 1 ? ( -
-
- - Via{' '} - {step.toolDetails.name}{' '} - {step.toolDetails.name} - - - Bridge {step.action.fromToken.symbol} - -
-
- ) : ( + const { srcStep, bridgeStep, dstStep } = useMemo(() => { + const bridgeIndex = step.includedSteps.findIndex( + (_step) => _step.type === 'cross', + ) + return { + srcStep: step.includedSteps[bridgeIndex - 1], + bridgeStep: step.includedSteps[bridgeIndex], + dstStep: step.includedSteps[bridgeIndex + 1], + } + }, [step]) + + return (
- +
- {step.includedSteps.map((_step) => { - return ( - - {_step.type === 'swap' ? ( - - ) : _step.type === 'cross' ? ( - - ) : null} - - ) - })} + {srcStep ? ( + + ) : ( + + Amount.fromRawAmount( + step.action.fromToken.address === zeroAddress + ? Native.onChain(step.action.fromToken.chainId) + : new Token(step.action.fromToken), + step.action.fromAmount, + ), + [step], + )} + /> + )} + + {dstStep ? ( + + ) : ( + + Amount.fromRawAmount( + step.action.toToken.address === zeroAddress + ? Native.onChain(step.action.toToken.chainId) + : new Token(step.action.toToken), + step.estimate.toAmount, + ), + [step], + )} + /> + )}
) @@ -107,49 +124,66 @@ const VerticalDivider: FC<{ className?: string; count: number }> = ({ ) } +const SendAction: FC<{ + label: 'From' | 'To' + amount: Amount +}> = ({ label, amount }) => { + const chain = useMemo( + () => Chain.fromChainId(amount.currency.chainId)?.name?.toUpperCase(), + [amount], + ) + + return ( +
+ + {label}{' '} + {' '} + {chain} + + + {`${label === 'From' ? 'Send' : 'Receive'} ${amount.toSignificant(6)} ${ + amount.currency.symbol + }`} + +
+ ) +} + const SwapAction: FC<{ + label: 'From' | 'To' action: CrossChainAction estimate: CrossChainEstimate - chainId0: ChainId -}> = ({ chainId0, action, estimate }) => { - const { fromAmount, toAmount, label, chain, isWrap, isUnwrap } = - useMemo(() => { - const [label, chain] = - chainId0 === action.fromToken.chainId - ? [ - 'From', - Chain.fromChainId(action.fromToken.chainId)?.name?.toUpperCase(), - ] - : [ - 'To', - Chain.fromChainId(action.toToken.chainId)?.name?.toUpperCase(), - ] +}> = ({ label, action, estimate }) => { + const { fromAmount, toAmount, chain, isWrap, isUnwrap } = useMemo(() => { + const fromToken = + action.fromToken.address === zeroAddress + ? Native.onChain(action.fromToken.chainId) + : new Token(action.fromToken) - const fromToken = - action.fromToken.address === zeroAddress - ? Native.onChain(action.fromToken.chainId) - : new Token(action.fromToken) + const toToken = + action.toToken.address === zeroAddress + ? Native.onChain(action.toToken.chainId) + : new Token(action.toToken) - const toToken = - action.toToken.address === zeroAddress - ? Native.onChain(action.toToken.chainId) - : new Token(action.toToken) + const chain = Chain.fromChainId( + label === 'From' ? fromToken.chainId : toToken.chainId, + )?.name?.toUpperCase() - return { - fromAmount: Amount.fromRawAmount(fromToken, action.fromAmount), - toAmount: Amount.fromRawAmount(toToken, estimate.toAmount), - label, - chain, - isWrap: fromToken.isNative && fromToken.wrapped.equals(toToken), - isUnwrap: toToken.isNative && toToken.wrapped.equals(fromToken), - } - }, [ - action.fromToken, - action.toToken, - action.fromAmount, - estimate.toAmount, - chainId0, - ]) + return { + fromAmount: Amount.fromRawAmount(fromToken, action.fromAmount), + toAmount: Amount.fromRawAmount(toToken, estimate.toAmount), + label, + chain, + isWrap: fromToken.isNative && fromToken.wrapped.equals(toToken), + isUnwrap: toToken.isNative && toToken.wrapped.equals(fromToken), + } + }, [ + action.fromToken, + action.toToken, + action.fromAmount, + estimate.toAmount, + label, + ]) return (
@@ -163,12 +197,7 @@ const SwapAction: FC<{ {chain} - {label === 'To' ? ( - <> - Receive {formatNumber(toAmount.toExact())}{' '} - {toAmount.currency.symbol} - - ) : isWrap ? ( + {isWrap ? ( <>Wrap {toAmount.currency.symbol} ) : isUnwrap ? ( <>Unwrap {fromAmount.currency.symbol} From 733399b3086a982c56bb4888c9eef4f28eb47aae Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Fri, 10 Jan 2025 18:54:53 +0700 Subject: [PATCH 19/26] chore: update xswap cache & refetch rate --- .../(evm)/api/cross-chain/routes/route.ts | 4 +-- .../(evm)/api/cross-chain/step/route.ts | 2 +- .../useCrossChainTradeRoutes.ts | 2 +- .../useCrossChainTradeStep.ts | 5 +-- .../cross-chain-swap-trade-review-dialog.tsx | 35 ++++++++++++++----- 5 files changed, 32 insertions(+), 16 deletions(-) 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 index 9a4f52fc24..7198b19dd3 100644 --- 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 @@ -37,7 +37,7 @@ const schema = z.object({ order: z.enum(['CHEAPEST', 'FASTEST']).optional(), }) -export const revalidate = 600 +export const revalidate = 20 export async function GET(request: NextRequest) { const params = Object.fromEntries(request.nextUrl.searchParams.entries()) @@ -74,7 +74,7 @@ export async function GET(request: NextRequest) { return Response.json(await response.json(), { status: response.status, headers: { - 'Cache-Control': 'max-age=60, stale-while-revalidate=600', + '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 index ed06e8b9d3..8eb9cf4ec4 100644 --- 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 @@ -44,7 +44,7 @@ export async function POST(request: NextRequest) { return Response.json(await response.json(), { status: response.status, headers: { - 'Cache-Control': 'max-age=10, stale-while-revalidate=60', + 'Cache-Control': 's-maxage=8, stale-while-revalidate=10', }, }) } 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 index 3ae8bbbf1f..6eee8c965b 100644 --- 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 @@ -72,7 +72,7 @@ export const useCrossChainTradeRoutes = ({ return routes }, - staleTime: query?.staleTime ?? 1000 * 15, // 15s + refetchInterval: query?.refetchInterval ?? 1000 * 20, // 20s enabled: query?.enabled !== false && Boolean(params.toToken && params.fromAmount?.greaterThan(0)), 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 index 0b55e512eb..fe31038cff 100644 --- 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 @@ -41,10 +41,7 @@ export const useCrossChainTradeStep = ({ headers: { 'Content-Type': 'application/json', }, - body: stringify({ - ...step, - // slippage: step.action.slippage // TODO: CHECK IF NEEDED HERE - }), + body: stringify(step), } const response = await fetch(url, options) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx index b68a2f1cf5..39721fb8d1 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx @@ -14,6 +14,8 @@ import { useTrace, } from '@sushiswap/telemetry' import { + Button, + Collapsible, DialogClose, DialogContent, DialogCustom, @@ -24,14 +26,13 @@ import { DialogReview, DialogTitle, DialogType, + Dots, + List, Message, SelectIcon, + SkeletonText, + useDialog, } from '@sushiswap/ui' -import { Collapsible } from '@sushiswap/ui' -import { Button } from '@sushiswap/ui' -import { Dots } from '@sushiswap/ui' -import { List } from '@sushiswap/ui' -import { SkeletonText } from '@sushiswap/ui' import { nanoid } from 'nanoid' import React, { FC, @@ -88,6 +89,18 @@ import { 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() @@ -107,11 +120,17 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ const client0 = usePublicClient({ chainId: chainId0 }) const client1 = usePublicClient({ chainId: chainId1 }) 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), + enabled: Boolean( + approved && address && (confirmDialogOpen || reviewDialogOpen), + ), }, }) @@ -534,7 +553,7 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ ) return ( - + <> {({ confirm }) => ( <> @@ -958,6 +977,6 @@ export const CrossChainSwapTradeReviewDialog: FC<{ children: ReactNode }> = ({ - + ) } From c5862e55f602d0cea7415417f43192c0502d0483 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Fri, 10 Jan 2025 18:55:14 +0700 Subject: [PATCH 20/26] fix: selectedBridge --- .../cross-chain/derivedstate-cross-chain-swap-provider.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx b/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx index d47496750a..95435987a3 100644 --- a/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx +++ b/apps/web/src/ui/swap/cross-chain/derivedstate-cross-chain-swap-provider.tsx @@ -305,6 +305,11 @@ const DerivedstateCrossChainSwapProvider: FC< [_token0, swapAmountString], ) + // biome-ignore lint/correctness/useExhaustiveDependencies: + useEffect(() => { + setSelectedBridge(undefined) + }, [swapAmount, routeOrder]) + return ( { @@ -395,7 +400,7 @@ const useCrossChainTradeRoutes = () => { if ( query.data?.length && (typeof selectedBridge === 'undefined' || - !query.data?.find((route) => route.steps[0].tool === selectedBridge)) + !query.data.find((route) => route.steps[0].tool === selectedBridge)) ) { setSelectedBridge(query.data[0].steps[0].tool) } From a8d2b4106900b4c0afbd522a580ad17f579c0683 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Fri, 10 Jan 2025 19:06:44 +0700 Subject: [PATCH 21/26] chore: placeholder data in XSwap review dialog --- .../cross-chain-swap-trade-review-dialog.tsx | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx index 39721fb8d1..4528edb864 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx @@ -125,7 +125,7 @@ const _CrossChainSwapTradeReviewDialog: FC<{ const { open: reviewDialogOpen } = useDialog(DialogType.Review) const { data: selectedRoute } = useSelectedCrossChainTradeRoute() - const { data: step, isError: isStepQueryError } = useCrossChainTradeStep({ + const { data: _step, isError: isStepQueryError } = useCrossChainTradeStep({ step: selectedRoute?.steps?.[0], query: { enabled: Boolean( @@ -134,6 +134,22 @@ const _CrossChainSwapTradeReviewDialog: FC<{ }, }) + 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() const { refetchChain: refetchBalances } = useRefetchBalances() From 3372761588e3c59fa2a5329eef2c84632597a248 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Mon, 13 Jan 2025 13:38:04 +0700 Subject: [PATCH 22/26] fix: handle unpriced token --- .../swap/cross-chain/cross-chain-swap-route-card.tsx | 10 +++++++++- .../cross-chain-swap-trade-review-dialog.tsx | 8 +++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx index 7b9e007400..203bb65aaf 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-card.tsx @@ -116,7 +116,15 @@ export const CrossChainSwapRouteCard: FC = ({
)}
- {`≈ ${amountOutUSD} after fees`} + + {amountOutUSD ? ( + {`≈ ${amountOutUSD} after fees`} + ) : ( + + + + )} +
{route.tags?.includes(order) ? ( diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx index 4528edb864..6b5c99d496 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-trade-review-dialog.tsx @@ -633,11 +633,9 @@ const _CrossChainSwapTradeReviewDialog: FC<{ subtitle="The impact your trade has on the market price of this pool." > {!step?.priceImpact ? ( - + + + ) : ( `${ step.priceImpact.lessThan(ZERO) From 813394978f7e085dcc5d43f27d88742b6b3ac539 Mon Sep 17 00:00:00 2001 From: Daniel Reinoso Date: Mon, 13 Jan 2025 13:32:39 -0300 Subject: [PATCH 23/26] feat: show native or wrapped symbol when collecting fees --- .../ConcentratedLiquidityCollectWidget.tsx | 30 ++++++++++++------- .../ConcentratedLiquidityRemoveWidget.tsx | 2 +- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx index 6e295c45a9..3b207f9c4e 100644 --- a/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx +++ b/apps/web/src/ui/pool/ConcentratedLiquidityCollectWidget.tsx @@ -55,13 +55,19 @@ export const ConcentratedLiquidityCollectWidget: FC< ) }, [token0, token1, nativeToken]) - const expectedToken0 = useMemo(() => { - return !token0 || receiveWrapped ? token0?.wrapped : unwrapToken(token0) - }, [token0, receiveWrapped]) + const expectedAmount0 = useMemo(() => { + const expectedToken0 = + !token0 || receiveWrapped ? token0?.wrapped : unwrapToken(token0) + if (amounts[0] === undefined || !expectedToken0) return undefined + return Amount.fromRawAmount(expectedToken0, amounts[0].quotient) + }, [token0, receiveWrapped, amounts]) - const expectedToken1 = useMemo(() => { - return !token1 || receiveWrapped ? token1?.wrapped : unwrapToken(token1) - }, [token1, receiveWrapped]) + const expectedAmount1 = useMemo(() => { + const expectedToken1 = + !token1 || receiveWrapped ? token1?.wrapped : unwrapToken(token1) + if (amounts[1] === undefined || !expectedToken1) return undefined + return Amount.fromRawAmount(expectedToken1, amounts[1].quotient) + }, [token1, receiveWrapped, amounts]) return ( <> @@ -69,20 +75,22 @@ export const ConcentratedLiquidityCollectWidget: FC< Tokens {positionHasNativeToken ? (
- {`Receive ${nativeToken.wrapped.symbol} as ${nativeToken.symbol}`} + {`Receive ${nativeToken.wrapped.symbol} instead of ${nativeToken.symbol}`} diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx index 867e333404..57aa8daba5 100644 --- a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx +++ b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx @@ -593,7 +593,7 @@ export const ConcentratedLiquidityRemoveWidget: FC< {positionHasNativeToken ? (
- {`Receive ${nativeToken.wrapped.symbol} as ${nativeToken.symbol}`} + {`Receive ${nativeToken.wrapped.symbol} instead of ${nativeToken.symbol}`} Date: Tue, 14 Jan 2025 08:53:09 -0300 Subject: [PATCH 24/26] feat: show native or wrapped symbol when decreasing liquidity --- .../ConcentratedLiquidityRemoveWidget.tsx | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx index 57aa8daba5..dc1bd826eb 100644 --- a/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx +++ b/apps/web/src/ui/pool/ConcentratedLiquidityRemoveWidget.tsx @@ -163,12 +163,16 @@ export const ConcentratedLiquidityRemoveWidget: FC< } }, []) + const [expectedToken0, expectedToken1] = useMemo(() => { + const expectedToken0 = + !token0 || receiveWrapped ? token0?.wrapped : unwrapToken(token0) + const expectedToken1 = + !token1 || receiveWrapped ? token1?.wrapped : unwrapToken(token1) + return [expectedToken0, expectedToken1] + }, [token0, token1, receiveWrapped]) + const [feeValue0, feeValue1] = useMemo(() => { - if (positionDetails && token0 && token1) { - const expectedToken0 = - !token0 || receiveWrapped ? token0?.wrapped : unwrapToken(token0) - const expectedToken1 = - !token1 || receiveWrapped ? token1?.wrapped : unwrapToken(token1) + if (positionDetails && expectedToken0 && expectedToken1) { const feeValue0 = positionDetails.fees ? Amount.fromRawAmount(expectedToken0, positionDetails.fees[0]) : undefined @@ -180,7 +184,7 @@ export const ConcentratedLiquidityRemoveWidget: FC< } return [undefined, undefined] - }, [positionDetails, token0, token1, receiveWrapped]) + }, [positionDetails, expectedToken0, expectedToken1]) const nativeToken = useMemo(() => Native.onChain(chainId), [chainId]) @@ -203,11 +207,6 @@ export const ConcentratedLiquidityRemoveWidget: FC< ? liquidityPercentage.multiply(position.amount1.quotient).quotient : undefined - const expectedToken0 = - receiveWrapped && token0 ? unwrapToken(token0) : token0 - const expectedToken1 = - receiveWrapped && token1 ? unwrapToken(token1) : token1 - const liquidityValue0 = expectedToken0 && typeof discountedAmount0 === 'bigint' ? Amount.fromRawAmount(expectedToken0, discountedAmount0) @@ -273,10 +272,9 @@ export const ConcentratedLiquidityRemoveWidget: FC< position, positionDetails, slippageTolerance, - token0, - token1, debouncedValue, - receiveWrapped, + expectedToken0, + expectedToken1, ]) const { isError: isSimulationError } = useCall({ @@ -525,10 +523,7 @@ export const ConcentratedLiquidityRemoveWidget: FC< {position?.amount0 && ( - +
${fiatAmountsAsNumber[0].toFixed(2)} @@ -557,10 +552,7 @@ export const ConcentratedLiquidityRemoveWidget: FC< )} {position?.amount1 && ( - +
${fiatAmountsAsNumber[1].toFixed(2)} From 5bc73d8bfa3c5de26a2e9c077b4edded53141443 Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Wed, 15 Jan 2025 19:48:30 +0700 Subject: [PATCH 25/26] chore: xswap route selector loading animation --- .../cross-chain-swap-route-selector.tsx | 89 ++++++------------- apps/web/tailwind.config.js | 13 +-- 2 files changed, 32 insertions(+), 70 deletions(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx index 7438d7c794..fd09bb62aa 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx @@ -155,7 +155,7 @@ const DesktopRouteSelector: FC = ({ - +
@@ -272,7 +272,7 @@ const MobileRouteSelector: FC = ({ - +
@@ -288,66 +288,33 @@ const MobileRouteSelector: FC = ({ ) } -const RouteSelectorCarousel = () => { +const SUSHI_IMGS = [ + 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-0.png', + 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-1.png', + 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-2.png', + 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-3.png', + 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-4.png', + 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-5.png', + 'https://cdn.sushi.com/image/upload/v1734107194/sushi-plate-6.png', +] + +const DUP_SUSHI_IMAGES = [...SUSHI_IMGS, ...SUSHI_IMGS] + +const RouteSelectorMarquee = () => { return ( -
-
- sushi - sushi - sushi - sushi - sushi - sushi - sushi +
+
+ {DUP_SUSHI_IMAGES.map((img, i) => ( + sushi + ))}
) diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index cf5388ea2e..2dd65fb96b 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -16,18 +16,13 @@ const tailwindConfig = { 'stroke-dashoffset': '0', }, }, - carouselSlide: { - '0%': { transform: 'translateX(0%)' }, - '28.57%': { transform: 'translateX(0%)' }, - '33.33%': { transform: 'translateX(-50%)' }, - '61.90%': { transform: 'translateX(-50%)' }, - '66.66%': { transform: 'translateX(-100%)' }, - '95.23%': { transform: 'translateX(-100%)' }, - '100%': { transform: 'translateX(0%)' }, + marquee: { + '0%': { transform: 'translateX(0)' }, + '100%': { transform: 'translateX(-833px)' }, }, }, animation: { - carouselSlide: 'carouselSlide 6s ease-in-out infinite', + marquee: 'marquee 5s linear infinite', }, }, }, From e422bf93497232021b2b465a573c2d98cfb4cb7a Mon Sep 17 00:00:00 2001 From: 0xMasayoshi <0xMasayoshi@protonmail.com> Date: Mon, 20 Jan 2025 14:34:04 +0700 Subject: [PATCH 26/26] chore: update No Routes Found view --- .../cross-chain-swap-route-selector.tsx | 54 +++++++++++++++---- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx index fd09bb62aa..22672c080d 100644 --- a/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx +++ b/apps/web/src/ui/swap/cross-chain/cross-chain-swap-route-selector.tsx @@ -23,7 +23,7 @@ import { } from '@sushiswap/ui' import { QueryStatus } from '@tanstack/react-query' import Image from 'next/image' -import { FC, useState } from 'react' +import { FC, useId, useState } from 'react' import { CrossChainRoute, CrossChainRouteOrder } from 'src/lib/swap/cross-chain' import { useSidebar } from 'src/ui/sidebar' import { CrossChainSwapRouteCard } from './cross-chain-swap-route-card' @@ -131,17 +131,15 @@ const DesktopRouteSelector: FC = ({ Select A Route - No Routes Found + - - No Routes Found. + + No Routes Found. + + The problem might be due to high transaction fees, not enough + liquidity, the amount being too small, or no available routes for + the combination you selected. + ) : ( @@ -319,3 +317,37 @@ const RouteSelectorMarquee = () => {
) } + +const NoRoutesFound = () => { + const id = useId() + + return ( + + + + + + + + + + + + ) +}