diff --git a/api/_dexes/cross-swap-service.ts b/api/_dexes/cross-swap-service.ts index bb42dc439..51ba58f83 100644 --- a/api/_dexes/cross-swap-service.ts +++ b/api/_dexes/cross-swap-service.ts @@ -25,7 +25,7 @@ import { import { getMultiCallHandlerAddress } from "../_multicall-handler"; import { getIndirectBridgeQuoteMessage, - getIndirectDestinationRoutes, + getIndirectDestinationRoute, } from "./utils-b2bi"; import { InvalidParamError, @@ -228,21 +228,19 @@ export async function getCrossSwapQuotesForExactInputB2BI( ): Promise { const { depositEntryPoint } = _prepCrossSwapQuotesRetrievalB2B(crossSwap); - const indirectDestinationRoutes = getIndirectDestinationRoutes({ + const indirectDestinationRoute = getIndirectDestinationRoute({ originChainId: crossSwap.inputToken.chainId, destinationChainId: crossSwap.outputToken.chainId, inputToken: crossSwap.inputToken.address, outputToken: crossSwap.outputToken.address, }); - if (indirectDestinationRoutes.length === 0) { + if (!indirectDestinationRoute) { throw new InvalidParamError({ message: "No indirect bridge routes found to specified destination chain", }); } - const [indirectDestinationRoute] = indirectDestinationRoutes; - // For EXACT_INPUT, we need to convert the amount to the intermediary output token decimals // to get the initial bridgeable output amount. let bridgeableOutputAmount = ConvertDecimals( @@ -329,21 +327,19 @@ export async function getCrossSwapQuotesForOutputB2BI( ): Promise { const { depositEntryPoint } = _prepCrossSwapQuotesRetrievalB2B(crossSwap); - const indirectDestinationRoutes = getIndirectDestinationRoutes({ + const indirectDestinationRoute = getIndirectDestinationRoute({ originChainId: crossSwap.inputToken.chainId, destinationChainId: crossSwap.outputToken.chainId, inputToken: crossSwap.inputToken.address, outputToken: crossSwap.outputToken.address, }); - if (indirectDestinationRoutes.length === 0) { + if (!indirectDestinationRoute) { throw new InvalidParamError({ message: "No indirect bridge routes found to specified destination chain", }); } - const [indirectDestinationRoute] = indirectDestinationRoutes; - const outputAmountWithAppFee = crossSwap.appFeePercent ? addMarkupToAmount(crossSwap.amount, crossSwap.appFeePercent) : crossSwap.amount; diff --git a/api/_dexes/utils-b2bi.ts b/api/_dexes/utils-b2bi.ts index 485b0d542..16e3fb96a 100644 --- a/api/_dexes/utils-b2bi.ts +++ b/api/_dexes/utils-b2bi.ts @@ -28,19 +28,19 @@ export function isIndirectDestinationRouteSupported(params: { inputToken: string; outputToken: string; }) { - return getIndirectDestinationRoutes(params).length > 0; + return !!getIndirectDestinationRoute(params); } -export function getIndirectDestinationRoutes(params: { +export function getIndirectDestinationRoute(params: { originChainId: number; destinationChainId: number; inputToken: string; outputToken: string; -}): IndirectDestinationRoute[] { +}): IndirectDestinationRoute | undefined { const indirectChainDestination = indirectChains.find( (chain) => chain.chainId === params.destinationChainId && - chain.intermediaryChains && + chain.intermediaryChain && chain.outputTokens.some( (token) => token.address.toLowerCase() === params.outputToken.toLowerCase() @@ -48,107 +48,92 @@ export function getIndirectDestinationRoutes(params: { ); if (!indirectChainDestination) { - return []; + return; } - const indirectDestinationRoutes = - indirectChainDestination.intermediaryChains.flatMap( - (_intermediaryChainId) => { - // Check if the indirect destination chain has token enabled - const isIntermediaryOutputTokenEnabled = - indirectChainDestination.outputTokens.some( - (token) => token.address === params.outputToken - ); - if (!isIntermediaryOutputTokenEnabled) { - return []; - } + const intermediaryChainId = indirectChainDestination.intermediaryChain; - // Check if input token is known - const inputToken = getTokenByAddress( - params.inputToken, - params.originChainId - ); - if (!inputToken) { - return []; - } + // Check if the indirect destination chain has token enabled + const isIntermediaryOutputTokenEnabled = + indirectChainDestination.outputTokens.some( + (token) => token.address === params.outputToken + ); + if (!isIntermediaryOutputTokenEnabled) { + return; + } - // Check if the indirect destination chain supports the intermediary chain - const indirectOutputToken = getTokenByAddress( - params.outputToken, - params.destinationChainId - ); - if (!indirectOutputToken) { - return []; - } + // Check if input token is known + const inputToken = getTokenByAddress(params.inputToken, params.originChainId); + if (!inputToken) { + return; + } - // Check if L1 token is known - const l1TokenAddress = - TOKEN_SYMBOLS_MAP[inputToken.symbol as keyof typeof TOKEN_SYMBOLS_MAP] - ?.addresses[HUB_POOL_CHAIN_ID]; - if (!l1TokenAddress) { - return []; - } - const l1Token = getTokenByAddress(l1TokenAddress, HUB_POOL_CHAIN_ID); - if (!l1Token) { - return []; - } + // Check if the indirect destination chain supports the intermediary chain + const indirectOutputToken = getTokenByAddress( + params.outputToken, + params.destinationChainId + ); + if (!indirectOutputToken) { + return; + } - // Check if intermediary output token is known - const intermediaryOutputTokenAddress = - l1Token.addresses[_intermediaryChainId]; - if (!intermediaryOutputTokenAddress) { - return []; - } - const intermediaryOutputToken = getTokenByAddress( - intermediaryOutputTokenAddress, - _intermediaryChainId - ); - if (!intermediaryOutputToken) { - return []; - } + // Check if L1 token is known + const l1TokenAddress = + TOKEN_SYMBOLS_MAP[inputToken.symbol as keyof typeof TOKEN_SYMBOLS_MAP] + ?.addresses[HUB_POOL_CHAIN_ID]; + if (!l1TokenAddress) { + return; + } + const l1Token = getTokenByAddress(l1TokenAddress, HUB_POOL_CHAIN_ID); + if (!l1Token) { + return; + } - // Check if there is a route from the origin chain to the intermediary chain - if ( - !isRouteEnabled( - params.originChainId, - _intermediaryChainId, - params.inputToken, - intermediaryOutputTokenAddress - ) - ) { - return []; - } + // Check if intermediary output token is known + const intermediaryOutputTokenAddress = l1Token.addresses[intermediaryChainId]; + if (!intermediaryOutputTokenAddress) { + return; + } + const intermediaryOutputToken = getTokenByAddress( + intermediaryOutputTokenAddress, + indirectChainDestination.intermediaryChain + ); + if (!intermediaryOutputToken) { + return; + } - return { - inputToken: { - symbol: inputToken.symbol, - name: inputToken.name, - decimals: inputToken.decimals, - address: inputToken.addresses[params.originChainId], - chainId: params.originChainId, - coingeckoId: inputToken.coingeckoId, - }, - intermediaryOutputToken: { - symbol: intermediaryOutputToken.symbol, - name: intermediaryOutputToken.name, - decimals: intermediaryOutputToken.decimals, - address: intermediaryOutputToken.addresses[_intermediaryChainId], - chainId: _intermediaryChainId, - coingeckoId: intermediaryOutputToken.coingeckoId, - }, - outputToken: { - symbol: indirectOutputToken.symbol, - name: indirectOutputToken.name, - decimals: indirectOutputToken.decimals, - address: indirectOutputToken.addresses[params.destinationChainId], - chainId: params.destinationChainId, - coingeckoId: indirectOutputToken.coingeckoId, - }, - }; - } - ); + // Check if there is a route from the origin chain to the intermediary chain + if ( + !isRouteEnabled( + params.originChainId, + intermediaryChainId, + params.inputToken, + intermediaryOutputTokenAddress + ) + ) { + return; + } - return indirectDestinationRoutes; + return { + inputToken: { + symbol: inputToken.symbol, + decimals: inputToken.decimals, + address: inputToken.addresses[params.originChainId], + chainId: params.originChainId, + }, + intermediaryOutputToken: { + symbol: intermediaryOutputToken.symbol, + decimals: intermediaryOutputToken.decimals, + address: intermediaryOutputToken.addresses[intermediaryChainId], + chainId: intermediaryChainId, + }, + outputToken: { + symbol: indirectOutputToken.symbol, + decimals: indirectOutputToken.decimals, + address: indirectOutputToken.addresses[params.destinationChainId], + chainId: params.destinationChainId, + }, + }; } export function getIndirectBridgeQuoteMessage( @@ -227,7 +212,7 @@ function _buildIndirectBridgeQuoteMessageToHyperCore( function _buildBridgeQuoteMessageToHyperCore( crossSwap: CrossSwap, bridgeableOutputAmount: BigNumber, - indirectDestinationRoute: ReturnType[0], + indirectDestinationRoute: IndirectDestinationRoute, appFee?: AppFee ) { const { diff --git a/scripts/chain-configs/hypercore/index.ts b/scripts/chain-configs/hypercore/index.ts index 2361b9617..e89ba0a1c 100644 --- a/scripts/chain-configs/hypercore/index.ts +++ b/scripts/chain-configs/hypercore/index.ts @@ -4,7 +4,7 @@ import { ChainConfig } from "../types"; export default { chainId: 1337, // Arbitrary chain id for HyperCore name: "HyperCore", - fullName: "HyperCore", + fullName: "Hyperliquid", logoPath: "./assets/logo.svg", grayscaleLogoPath: "./assets/grayscale-logo.svg", spokePool: { @@ -14,12 +14,10 @@ export default { publicRpcUrl: "https://api.hyperliquid.xyz", blockExplorer: "https://app.hyperliquid.xyz/explorer", blockTimeSeconds: 1, - tokens: [], - inputTokens: [], - outputTokens: ["USDT-SPOT"], + tokens: ["USDT-SPOT"], enableCCTP: false, omitViemConfig: true, nativeToken: "HYPE", // HyperCore can only be reached via HyperEVM as an intermediary chain. - intermediaryChains: [CHAIN_IDs.HYPEREVM], + intermediaryChain: CHAIN_IDs.HYPEREVM, } as ChainConfig; diff --git a/scripts/chain-configs/types.ts b/scripts/chain-configs/types.ts index 6066fad8c..b0c11898c 100644 --- a/scripts/chain-configs/types.ts +++ b/scripts/chain-configs/types.ts @@ -27,19 +27,5 @@ export type ChainConfig = { toTokenSymbol: string; externalProjectId?: string; }[]; - intermediaryChains?: number[]; - outputTokens?: ( - | string - | { - symbol: string; - chainIds: number[]; - } - )[]; - inputTokens?: ( - | string - | { - symbol: string; - chainIds: number[]; - } - )[]; + intermediaryChain?: number; }; diff --git a/scripts/extern-configs/hyperliquid/index.ts b/scripts/extern-configs/hyperliquid/index.ts index 30bf63c0d..e33bb5e38 100644 --- a/scripts/extern-configs/hyperliquid/index.ts +++ b/scripts/extern-configs/hyperliquid/index.ts @@ -3,6 +3,7 @@ import { ExternalProjectConfig } from "../types"; export default { name: "Hyperliquid", + fullName: "Hyperliquid", projectId: "hyperliquid", explorer: "https://arbiscan.io", logoPath: "./assets/logo.svg", diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index df3eba3fc..5504cf468 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -210,8 +210,7 @@ const enabledRoutes = { }, routes: transformChainConfigs( enabledMainnetChainConfigs, - enabledMainnetExternalProjects, - enabledIndirectMainnetChainConfigs + enabledMainnetExternalProjects ), }, [CHAIN_IDs.SEPOLIA]: { @@ -244,18 +243,13 @@ const enabledRoutes = { }, spokePoolPeripheryAddresses: {}, swapProxyAddresses: {}, - routes: transformChainConfigs( - enabledSepoliaChainConfigs, - [], - enabledIndirectSepoliaChainConfigs - ), + routes: transformChainConfigs(enabledSepoliaChainConfigs, []), }, } as const; function transformChainConfigs( enabledChainConfigs: typeof enabledMainnetChainConfigs, - enabledExternalProjects: typeof enabledMainnetExternalProjects, - enabledIndirectChainConfigs: typeof enabledIndirectMainnetChainConfigs + enabledExternalProjects: typeof enabledMainnetExternalProjects ) { const transformedChainConfigs: { fromChain: number; @@ -726,7 +720,7 @@ async function generateRoutes(hubPoolChainId = 1) { ) || []; if (!chainKey) { throw new Error( - `Could not find INDIRECTchain key for chain ${chainConfig.chainId}` + `Could not find indirect chain key for chain ${chainConfig.chainId}` ); } const assetsBaseUrl = `https://raw.githubusercontent.com/across-protocol/frontend/master`; @@ -751,7 +745,7 @@ async function generateRoutes(hubPoolChainId = 1) { logoUrl: `${assetsBaseUrl}${path.resolve("/scripts/chain-configs/", chainKey.toLowerCase().replace("_", "-"), chainConfig.logoPath)}`, spokePool: chainConfig.spokePool.address, spokePoolBlock: chainConfig.spokePool.blockNumber, - intermediaryChains: chainConfig.intermediaryChains, + intermediaryChain: chainConfig.intermediaryChain, inputTokens: chainConfig.tokens.flatMap((token) => { try { if (typeof token === "string") { @@ -769,25 +763,23 @@ async function generateRoutes(hubPoolChainId = 1) { return []; } }), - outputTokens: (chainConfig.outputTokens ?? chainConfig.tokens).flatMap( - (token) => { - try { - if (typeof token === "string") { - return getTokenInfo(token); - } else { - if (token.chainIds.includes(chainConfig.chainId)) { - return getTokenInfo(token.symbol); - } - return []; + outputTokens: chainConfig.tokens.flatMap((token) => { + try { + if (typeof token === "string") { + return getTokenInfo(token); + } else { + if (token.chainIds.includes(chainConfig.chainId)) { + return getTokenInfo(token.symbol); } - } catch (e) { - console.warn( - `Could not find token info for ${token} on chain ${chainConfig.chainId}` - ); return []; } + } catch (e) { + console.warn( + `Could not find token info for ${token} on chain ${chainConfig.chainId}` + ); + return []; } - ), + }), }; }); writeFileSync( diff --git a/scripts/generate-swap-routes.ts b/scripts/generate-swap-routes.ts index 78aad575b..ca2ca7cf0 100644 --- a/scripts/generate-swap-routes.ts +++ b/scripts/generate-swap-routes.ts @@ -72,16 +72,22 @@ const enabledSwapRoutes: { }, }, [TOKEN_SYMBOLS_MAP.USDT.symbol]: { - // TODO: Enable once FE is able to handle USDT-SPOT and HyperCore - // all: { - // enabledDestinationChains: [CHAIN_IDs.HYPERCORE], - // enabledOutputTokens: ["USDT-SPOT"], - // }, + all: { + disabledOriginChains: [CHAIN_IDs.HYPEREVM], + enabledDestinationChains: [CHAIN_IDs.HYPERCORE], + enabledOutputTokens: ["USDT-SPOT"], + }, [CHAIN_IDs.MAINNET]: { enabledDestinationChains: [CHAIN_IDs.LENS], enabledOutputTokens: ["GHO"], }, }, + [TOKEN_SYMBOLS_MAP["USDT-BNB"].symbol]: { + [CHAIN_IDs.BSC]: { + enabledDestinationChains: [CHAIN_IDs.HYPERCORE], + enabledOutputTokens: ["USDT-SPOT"], + }, + }, [TOKEN_SYMBOLS_MAP.DAI.symbol]: { [CHAIN_IDs.MAINNET]: { enabledDestinationChains: [CHAIN_IDs.LENS], diff --git a/scripts/generate-ui-assets.ts b/scripts/generate-ui-assets.ts index 1be216049..2ec1e11f3 100644 --- a/scripts/generate-ui-assets.ts +++ b/scripts/generate-ui-assets.ts @@ -73,6 +73,7 @@ async function generateUiAssets() { nativeCurrencySymbol: "${chainConfig.nativeToken}", customRpcUrl: process.env.REACT_APP_CHAIN_${chainId}_CUSTOM_RPC_URL, pollingInterval: ${(chainConfig.blockTimeSeconds || 15) * 1000}, + ${chainConfig.intermediaryChain ? `intermediaryChain: ${chainConfig.intermediaryChain},` : ""} }; `); chainVarNames.push(chainVarName); diff --git a/src/assets/icons/info_filled.svg b/src/assets/icons/info_filled.svg new file mode 100644 index 000000000..edb048d46 --- /dev/null +++ b/src/assets/icons/info_filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/components/Alert/Alert.styles.ts b/src/components/Alert/Alert.styles.ts index 7f3fcf318..e3b44eb94 100644 --- a/src/components/Alert/Alert.styles.ts +++ b/src/components/Alert/Alert.styles.ts @@ -1,6 +1,6 @@ import styled from "@emotion/styled"; import { AlertStatusType } from "./Alert"; -import { ReactComponent as InfoIcon } from "assets/icons/info.svg"; +import { ReactComponent as InfoIcon } from "assets/icons/info_filled.svg"; import { ReactComponent as QuestionIcon } from "assets/icons/question-circle.svg"; import { QUERIESV2 } from "utils"; @@ -14,9 +14,9 @@ const AlertColors: Record< borderColor: "#3e4047", }, warn: { - bgColor: "rgba(249, 210, 108, 0.05)", - fontColor: "#f9d26c", - borderColor: "rgba(249, 210, 108, 0.1)", + bgColor: "rgba(255, 149, 0, 0.05)", + fontColor: "#FF9500", + borderColor: "rgba(255, 149, 0, 0.5)", }, danger: { bgColor: "rgba(249, 108, 108, 0.05)", @@ -93,9 +93,7 @@ export const StyledQuestionIcon = styled(QuestionIcon)` export const StyledInfoIcon = styled(InfoIcon)` flex-shrink: 0; - & path { - stroke: ${({ status }) => AlertColors[status].fontColor}; - } + color: ${({ status }) => AlertColors[status].fontColor}; height: 24px; width: 24px; diff --git a/src/components/GlobalStyles/reset.ts b/src/components/GlobalStyles/reset.ts index ed1b76e69..a70415f4e 100644 --- a/src/components/GlobalStyles/reset.ts +++ b/src/components/GlobalStyles/reset.ts @@ -125,4 +125,19 @@ export const reset = css` border-collapse: collapse; border-spacing: 0; } + + button, + input[type="button"], + input[type="submit"], + input[type="reset"] { + background: none; + border: none; + padding: 0; + margin: 0; + font: inherit; + color: inherit; + text-decoration: none; + cursor: pointer; + outline: none; + } `; diff --git a/src/constants/chains/configs.ts b/src/constants/chains/configs.ts index 720ecd35b..93079a361 100644 --- a/src/constants/chains/configs.ts +++ b/src/constants/chains/configs.ts @@ -463,7 +463,7 @@ export const bnbSmartChain_viem = defineChain({ export const hyperCore = { name: "HyperCore", - fullName: "HyperCore", + fullName: "Hyperliquid", chainId: 1337, logoURI: hyperCoreLogo, grayscaleLogoURI: hyperCoreGrayscaleLogo, @@ -476,6 +476,7 @@ export const hyperCore = { nativeCurrencySymbol: "HYPE", customRpcUrl: process.env.REACT_APP_CHAIN_1337_CUSTOM_RPC_URL, pollingInterval: 1000, + intermediaryChain: 999, }; export const hyperEvm = { diff --git a/src/constants/chains/index.ts b/src/constants/chains/index.ts index 626fa38e8..5473310f0 100644 --- a/src/constants/chains/index.ts +++ b/src/constants/chains/index.ts @@ -1,7 +1,9 @@ import { CHAIN_IDs } from "@across-protocol/constants"; import { chainConfigs } from "./configs"; -export type ChainInfo = (typeof chainConfigs)[0]; +export type ChainInfo = (typeof chainConfigs)[0] & { + intermediaryChain?: number; +}; export type ChainInfoList = ChainInfo[]; export type ChainInfoTable = Record; export type ChainId = (typeof CHAIN_IDs)[keyof typeof CHAIN_IDs]; diff --git a/src/constants/tokens.ts b/src/constants/tokens.ts index 860e0d939..720a61e19 100644 --- a/src/constants/tokens.ts +++ b/src/constants/tokens.ts @@ -88,6 +88,7 @@ export const orderedTokenLogos = { "USDC-BNB": usdcLogo, USDT: usdtLogo, "USDT-BNB": usdtLogo, + "USDT-SPOT": usdtLogo, DAI: daiLogo, USDB: usdbLogo, WBTC: wbtcLogo, diff --git a/src/data/indirect_chains_1.json b/src/data/indirect_chains_1.json index e4c57c5b9..9c505195f 100644 --- a/src/data/indirect_chains_1.json +++ b/src/data/indirect_chains_1.json @@ -7,8 +7,16 @@ "logoUrl": "https://raw.githubusercontent.com/across-protocol/frontend/master/scripts/chain-configs/hypercore/assets/logo.svg", "spokePool": "0x0000000000000000000000000000000000000000", "spokePoolBlock": 0, - "intermediaryChains": [999], - "inputTokens": [], + "intermediaryChain": 999, + "inputTokens": [ + { + "address": "0x200000000000000000000000000000000000010C", + "symbol": "USDT-SPOT", + "name": "Tether USD", + "decimals": 8, + "logoUrl": "https://raw.githubusercontent.com/across-protocol/frontend/master/src/assets/token-logos/usdt-spot.svg" + } + ], "outputTokens": [ { "address": "0x200000000000000000000000000000000000010C", diff --git a/src/data/universal-swap-routes_1.json b/src/data/universal-swap-routes_1.json index ec3b012a7..feaba89ce 100644 --- a/src/data/universal-swap-routes_1.json +++ b/src/data/universal-swap-routes_1.json @@ -299,6 +299,150 @@ "type": "universal-swap", "isNative": false }, + { + "fromChain": 1, + "toChain": 1337, + "fromTokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 10, + "toChain": 1337, + "fromTokenAddress": "0x94b008aA00579c1307B0EF2c499aD98a8ce58e58", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x6f26Bf09B1C792e3228e5467807a900A503c0281", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 137, + "toChain": 1337, + "fromTokenAddress": "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 42161, + "toChain": 1337, + "fromTokenAddress": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 324, + "toChain": 1337, + "fromTokenAddress": "0x493257fD37EDB34451f62EDf8D2a0C418852bA4C", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0xE0B015E54d54fc84a6cB9B666099c46adE9335FF", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 8453, + "toChain": 1337, + "fromTokenAddress": "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 59144, + "toChain": 1337, + "fromTokenAddress": "0xA219439258ca9da29E9Cc4cE5596924745e12B93", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x7E63A5f1a8F0B4d0934B2f2327DAED3F6bb2ee75", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 34443, + "toChain": 1337, + "fromTokenAddress": "0xf0F161fDA2712DB8b566946122a5af183995e2eD", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x3baD7AD0728f9917d1Bf08af5782dCbD516cDd96", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 1135, + "toChain": 1337, + "fromTokenAddress": "0x05D032ac25d322df992303dCa074EE7392C117b9", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x9552a0a6624A23B848060AE5901659CDDa1f83f8", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 534352, + "toChain": 1337, + "fromTokenAddress": "0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x3baD7AD0728f9917d1Bf08af5782dCbD516cDd96", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 9745, + "toChain": 1337, + "fromTokenAddress": "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x50039fAEfebef707cFD94D6d462fE6D10B39207a", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, + { + "fromChain": 56, + "toChain": 1337, + "fromTokenAddress": "0x55d398326f99059fF775485246999027B3197955", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT-BNB", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x4e8E101924eDE233C13e2D8622DC8aED2872d505", + "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "type": "universal-swap", + "isNative": false + }, { "fromChain": 1, "toChain": 232, diff --git a/src/hooks/useBridgeFees.ts b/src/hooks/useBridgeFees.ts index 9a180b221..821a1c828 100644 --- a/src/hooks/useBridgeFees.ts +++ b/src/hooks/useBridgeFees.ts @@ -45,6 +45,12 @@ export function useBridgeFees( const bridgeOutputTokenSymbol = didUniversalSwapLoad ? universalSwapQuote.steps.bridge.tokenOut.symbol : outputTokenSymbol; + const bridgeOriginChainId = didUniversalSwapLoad + ? universalSwapQuote.steps.bridge.tokenIn.chainId + : fromChainId; + const bridgeDestinationChainId = didUniversalSwapLoad + ? universalSwapQuote.steps.bridge.tokenOut.chainId + : toChainId; const recipientAddress = _recipientAddress ?? (chainIsSvm(toChainId) @@ -55,8 +61,8 @@ export function useBridgeFees( amount, bridgeInputTokenSymbol, bridgeOutputTokenSymbol, - fromChainId, - toChainId, + bridgeOriginChainId, + bridgeDestinationChainId, externalProjectId, recipientAddress ); diff --git a/src/hooks/useBridgeLimits.ts b/src/hooks/useBridgeLimits.ts index 595be7f53..d425046a3 100644 --- a/src/hooks/useBridgeLimits.ts +++ b/src/hooks/useBridgeLimits.ts @@ -43,11 +43,17 @@ export function useBridgeLimits( const bridgeOutputTokenSymbol = didUniversalSwapLoad ? universalSwapQuote.steps.bridge.tokenOut.symbol : outputTokenSymbol; + const bridgeOriginChainId = didUniversalSwapLoad + ? universalSwapQuote.steps.bridge.tokenIn.chainId + : fromChainId; + const bridgeDestinationChainId = didUniversalSwapLoad + ? universalSwapQuote.steps.bridge.tokenOut.chainId + : toChainId; const queryKey = bridgeLimitsQueryKey( bridgeInputTokenSymbol, bridgeOutputTokenSymbol, - fromChainId, - toChainId + bridgeOriginChainId, + bridgeDestinationChainId ); const { data: limits, ...delegated } = useQuery({ queryKey, diff --git a/src/hooks/useMustInitializeHyperliquid.ts b/src/hooks/useMustInitializeHyperliquid.ts new file mode 100644 index 000000000..d53b82297 --- /dev/null +++ b/src/hooks/useMustInitializeHyperliquid.ts @@ -0,0 +1,41 @@ +import { CHAIN_IDs } from "@across-protocol/constants"; +import { useQuery } from "@tanstack/react-query"; +import { accountExistsOnHyperCore, Route } from "utils"; + +export function useMustInitializeHyperliquid(params: { + account: string | undefined; + route: Route; +}) { + const { account, route } = params; + return useQuery({ + queryKey: [ + "accountExistsOnHyperCore", + account, + route.toChain, + route.fromTokenSymbol, + ], + queryFn: async () => { + if ( + !account || + !( + // only tell user to initialize for this specific route + ( + route.toChain === CHAIN_IDs.HYPERCORE && + route.toTokenSymbol === "USDT-SPOT" + ) + ) + ) { + return false; + } + const accountExists = await accountExistsOnHyperCore({ + account, + }); + + return !accountExists; + }, + enabled: + route.toChain === CHAIN_IDs.HYPERCORE && + route.toTokenSymbol === "USDT-SPOT" && + !!account, + }); +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9f21d0030..4e268c22a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -22,6 +22,7 @@ import MainnetUniversalSwapRoutes from "data/universal-swap-routes_1.json"; import SepoliaRoutes from "data/routes_11155111_0x14224e63716afAcE30C9a417E0542281869f7d9e.json"; import { Deposit } from "hooks/useDeposits"; +import indirectChains from "data/indirect_chains_1.json"; import { ChainId, ChainInfo, @@ -58,6 +59,14 @@ export { similarTokensMap, }; +export const INDIRECT_CHAINS = indirectChains.reduce( + (acc, chain) => { + acc[chain.chainId] = chain; + return acc; + }, + {} as Record +); + /* Colors and Media Queries section */ export const BREAKPOINTS = { tabletMin: 550, @@ -106,6 +115,9 @@ export const tokenList = [ } else if (symbol === "USDT-BNB") { name = "Tether USD (BNB)"; displaySymbol = "USDT"; + } else if (symbol === "USDT-SPOT") { + name = "Tether USD (SPOT)"; + displaySymbol = "USDT"; } return { diff --git a/src/utils/hyperliquid.ts b/src/utils/hyperliquid.ts index 9f3cedf61..2b6c939a0 100644 --- a/src/utils/hyperliquid.ts +++ b/src/utils/hyperliquid.ts @@ -3,6 +3,7 @@ import { CHAIN_IDs } from "@across-protocol/constants"; import { BigNumber, Contract, providers, Signer, utils } from "ethers"; import { compareAddressesSimple } from "./sdk"; import { getToken, hyperLiquidBridge2Address } from "./constants"; +import { getProvider } from "./providers"; export function isHyperLiquidBoundDeposit(deposit: Deposit) { if (deposit.destinationChainId !== CHAIN_IDs.ARBITRUM || !deposit.message) { @@ -106,3 +107,24 @@ export async function generateHyperLiquidPayload( return iface.encodeFunctionData("batchedDepositWithPermit", [[deposit]]); } + +// Contract used to check if an account exists on Hypercore. +const CORE_USER_EXISTS_PRECOMPILE_ADDRESS = + "0x0000000000000000000000000000000000000810"; + +export async function accountExistsOnHyperCore(params: { account: string }) { + const provider = getProvider(CHAIN_IDs.HYPEREVM); + const balanceCoreCalldata = utils.defaultAbiCoder.encode( + ["address"], + [params.account] + ); + const queryResult = await provider.call({ + to: CORE_USER_EXISTS_PRECOMPILE_ADDRESS, + data: balanceCoreCalldata, + }); + const decodedQueryResult = utils.defaultAbiCoder.decode( + ["bool"], + queryResult + ); + return Boolean(decodedQueryResult[0]); +} diff --git a/src/utils/serverless-api/mocked/swap-approval.ts b/src/utils/serverless-api/mocked/swap-approval.ts index c1136e388..6c8ebfae1 100644 --- a/src/utils/serverless-api/mocked/swap-approval.ts +++ b/src/utils/serverless-api/mocked/swap-approval.ts @@ -79,6 +79,18 @@ export async function swapApprovalApiCall( decimals: 18, symbol: params.inputToken, }, + inputToken: { + address: params.inputToken, + chainId: params.originChainId, + decimals: 18, + symbol: params.inputToken, + }, + outputToken: { + address: params.outputToken, + chainId: params.destinationChainId, + decimals: 18, + symbol: params.outputToken, + }, inputAmount: BigNumber.from(params.amount), expectedOutputAmount: BigNumber.from(params.amount), minOutputAmount: BigNumber.from(params.amount), diff --git a/src/utils/serverless-api/prod/swap-approval.ts b/src/utils/serverless-api/prod/swap-approval.ts index f23332e66..c3e8f50e7 100644 --- a/src/utils/serverless-api/prod/swap-approval.ts +++ b/src/utils/serverless-api/prod/swap-approval.ts @@ -80,6 +80,8 @@ export type SwapApprovalApiResponse = { expectedOutputAmount: string; minOutputAmount: string; expectedFillTime: number; + inputToken: SwapApiToken; + outputToken: SwapApiToken; swapTx: { simulationSuccess: boolean; chainId: number; @@ -194,6 +196,8 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { : undefined, }, refundToken: result.refundToken, + inputToken: result.inputToken, + outputToken: result.outputToken, inputAmount: BigNumber.from(result.inputAmount), expectedOutputAmount: BigNumber.from(result.expectedOutputAmount), minOutputAmount: BigNumber.from(result.minOutputAmount), diff --git a/src/views/Bridge/Bridge.tsx b/src/views/Bridge/Bridge.tsx index b6e03c545..82e5419db 100644 --- a/src/views/Bridge/Bridge.tsx +++ b/src/views/Bridge/Bridge.tsx @@ -43,6 +43,7 @@ const Bridge = () => { handleSelectToChain, handleSetNewSlippage, isQuoteLoading, + showHyperliquidWarning, } = useBridge(); const destinationChainEcosystem = getEcosystem(selectedRoute.toChain); @@ -92,6 +93,7 @@ const Bridge = () => { isQuoteLoading={isQuoteLoading} swapQuote={swapQuote} universalSwapQuote={universalSwapQuote} + showHyperliquidWarning={showHyperliquidWarning} /> diff --git a/src/views/Bridge/components/BridgeForm.tsx b/src/views/Bridge/components/BridgeForm.tsx index b9a88d5ba..a61373ae2 100644 --- a/src/views/Bridge/components/BridgeForm.tsx +++ b/src/views/Bridge/components/BridgeForm.tsx @@ -73,6 +73,7 @@ export type BridgeFormProps = { validationError?: AmountInputError; validationWarning?: AmountInputError; isQuoteLoading: boolean; + showHyperliquidWarning?: boolean; }; // If swap price impact is lower than this threshold, show a warning @@ -111,6 +112,7 @@ const BridgeForm = ({ validationError, validationWarning, isQuoteLoading, + showHyperliquidWarning, }: BridgeFormProps) => { const programName = chainIdToRewardsProgramName[selectedRoute.toChain]; const { connect: connectEVM } = useConnectionEVM(); @@ -273,6 +275,19 @@ const BridgeForm = ({ )} + {showHyperliquidWarning && ( + + + + You must initialize this account on Hyperliquid before bridging + USDT0.{" "} + onSelectInputToken("USDC")}> + Bridge USDC to initialize. + + + + + )} void; @@ -141,7 +141,10 @@ function ChainInfoElement({ /** * Filters supported chains based on external project constraints */ -function filterAvailableChains(fromOrTo: "from" | "to", selectedRoute: Route) { +function filterAvailableChains( + fromOrTo: "from" | "to", + selectedRoute: SelectedRoute +) { const isFrom = fromOrTo === "from"; let chains = getSupportedChains(fromOrTo); const { externalProjectId, fromChain, fromTokenSymbol } = selectedRoute; @@ -152,7 +155,15 @@ function filterAvailableChains(fromOrTo: "from" | "to", selectedRoute: Route) { } if (!isFrom) { - chains = chains.filter(({ projectId }) => { + chains = chains.filter(({ projectId, chainId }) => { + // FIXME: remove this hardcoded filter once we have a proper solution for HyperCore + if ( + ["USDC", "USDC-BNB"].includes(fromTokenSymbol) && + chainId === ChainId.HYPERCORE + ) { + return false; + } + if (!projectId) return true; const { intermediaryChain } = externConfigs[projectId]; diff --git a/src/views/Bridge/components/FeesCollapsible.tsx b/src/views/Bridge/components/FeesCollapsible.tsx index d478805c1..faf4ecf66 100644 --- a/src/views/Bridge/components/FeesCollapsible.tsx +++ b/src/views/Bridge/components/FeesCollapsible.tsx @@ -52,7 +52,7 @@ export type Props = { export function FeesCollapsible(props: Props) { const [isExpanded, setIsExpanded] = useState(false); - const { inputToken, bridgeToken } = getTokensForFeesCalc(props); + const { inputToken, bridgeToken, outputToken } = getTokensForFeesCalc(props); const { convertTokenToBaseCurrency: convertInputTokenToUsd } = useTokenConversion(inputToken.symbol, "usd"); @@ -63,7 +63,7 @@ export function FeesCollapsible(props: Props) { const { convertTokenToBaseCurrency: convertOutputTokenToUsd, convertBaseCurrencyToToken: convertUsdToOutputToken, - } = useTokenConversion(props.outputToken.symbol, "usd"); + } = useTokenConversion(outputToken.symbol, "usd"); const { bridgeFeeUsd, diff --git a/src/views/Bridge/hooks/useBridge.ts b/src/views/Bridge/hooks/useBridge.ts index 07002872c..3bdc464aa 100644 --- a/src/views/Bridge/hooks/useBridge.ts +++ b/src/views/Bridge/hooks/useBridge.ts @@ -13,6 +13,7 @@ import { useSelectRoute } from "./useSelectRoute"; import { useTransferQuote, type TransferQuote } from "./useTransferQuote"; import { useAmountInput } from "./useAmountInput"; import { validateBridgeAmount } from "../utils"; +import { useMustInitializeHyperliquid } from "hooks/useMustInitializeHyperliquid"; export function useBridge() { const [shouldUpdateQuote, setShouldUpdateQuote] = useState(true); @@ -78,6 +79,11 @@ export function useBridge() { ); const { data: transferQuote } = transferQuoteQuery; + const { data: showHyperliquidWarning } = useMustInitializeHyperliquid({ + account: recipient?.address, + route: selectedRoute, + }); + const { quotedFees, quotedSwap, @@ -193,5 +199,6 @@ export function useBridge() { handleSelectOutputToken, handleSetNewSlippage: setSwapSlippage, isQuoteLoading: isQuoteUpdating, + showHyperliquidWarning, }; } diff --git a/src/views/Bridge/hooks/useSelectRoute.ts b/src/views/Bridge/hooks/useSelectRoute.ts index c79fa391a..d942c6028 100644 --- a/src/views/Bridge/hooks/useSelectRoute.ts +++ b/src/views/Bridge/hooks/useSelectRoute.ts @@ -7,6 +7,7 @@ import { trackQuickSwap, similarTokensMap, externalProjectNameToId, + ChainId, } from "utils"; import { useAmplitude, usePrevious } from "hooks"; @@ -66,19 +67,30 @@ export function useSelectRoute() { const handleSelectInputToken = useCallback( (inputOrSwapTokenSymbol: string) => { - const baseFilter = { + let baseFilter = { fromChain: selectedRoute.fromChain, toChain: selectedRoute.toChain, outputTokenSymbol: getOutputTokenSymbol( inputOrSwapTokenSymbol, - selectedRoute.toTokenAddress + selectedRoute.toTokenSymbol ), + externalProjectId: selectedRoute.externalProjectId, }; + if (selectedRoute.externalProjectId === "hyperliquid") { + baseFilter.toChain = ChainId.HYPERCORE; + baseFilter.externalProjectId = undefined; + } else if (selectedRoute.toChain === ChainId.HYPERCORE) { + baseFilter.toChain = ChainId.ARBITRUM; + baseFilter.externalProjectId = "hyperliquid"; + } const _route = - findNextBestRoute(["inputTokenSymbol", "fromChain", "toChain"], { - ...baseFilter, - inputTokenSymbol: inputOrSwapTokenSymbol, - }) || + findNextBestRoute( + ["inputTokenSymbol", "fromChain", "toChain", "externalProjectId"], + { + ...baseFilter, + inputTokenSymbol: inputOrSwapTokenSymbol, + } + ) || findNextBestRoute(["swapTokenSymbol", "fromChain", "toChain"], { ...baseFilter, swapTokenSymbol: inputOrSwapTokenSymbol, diff --git a/src/views/Bridge/utils.ts b/src/views/Bridge/utils.ts index 8b5a3897d..6ceb9d4b4 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -21,6 +21,7 @@ import { TokenInfo, isNonEthChain, isStablecoin, + ChainId, } from "utils"; import { SwapQuoteApiResponse } from "utils/serverless-api/prod/swap-quote"; @@ -455,8 +456,10 @@ export function getAvailableInputTokens( .filter( (route) => route.fromChain === selectedFromChain && - route.toChain === selectedToChain && - route.externalProjectId === externalProjectId + ((route.toChain === selectedToChain && + route.externalProjectId === externalProjectId) || + (selectedToChain === ChainId.HYPERCORE && + route.externalProjectId === "hyperliquid")) ) .map((route) => getToken(route.fromTokenSymbol)); const swapTokens = swapRoutes @@ -471,8 +474,10 @@ export function getAvailableInputTokens( .filter( (route) => route.fromChain === selectedFromChain && - route.toChain === selectedToChain && - route.externalProjectId === externalProjectId + ((route.toChain === selectedToChain && + route.externalProjectId === externalProjectId) || + (route.toChain === ChainId.HYPERCORE && + externalProjectId === "hyperliquid")) ) .map((route) => getToken(route.fromTokenSymbol)); return [...routeTokens, ...swapTokens, ...universalSwapTokens].filter( @@ -530,21 +535,22 @@ export const ChainType = { export type ChainTypeT = (typeof ChainType)[keyof typeof ChainType]; export function getSupportedChains(chainType: ChainTypeT = ChainType.ALL) { + const universalSwapRoutes = config.getUniversalSwapRoutes(); + const bridgeRoutes = config.getRoutes(); + const allRoutes = bridgeRoutes.concat(universalSwapRoutes); + let chainIds: number[] = []; switch (chainType) { case ChainType.FROM: - chainIds = enabledRoutes.map((route) => route.fromChain); + chainIds = allRoutes.map((route) => route.fromChain); break; case ChainType.TO: - chainIds = enabledRoutes.map((route) => route.toChain); + chainIds = allRoutes.map((route) => route.toChain); break; case ChainType.ALL: default: - chainIds = enabledRoutes.flatMap((route) => [ - route.fromChain, - route.toChain, - ]); + chainIds = allRoutes.flatMap((route) => [route.fromChain, route.toChain]); break; } @@ -774,7 +780,7 @@ function calcUniversalSwapFeeUsd(params: { return BigNumber.from(0); } const parsedAmount = BigNumber.from(params.parsedAmount || 0); - const { steps } = params.universalSwapQuote; + const { steps, expectedOutputAmount } = params.universalSwapQuote; const parsedInputAmountUsd = params.convertInputTokenToUsd(parsedAmount) || BigNumber.from(0); const originSwapFeeUsd = parsedInputAmountUsd.sub( @@ -785,10 +791,9 @@ function calcUniversalSwapFeeUsd(params: { params.convertBridgeTokenToUsd(steps.bridge.outputAmount) || BigNumber.from(0) ).sub( - params.convertOutputTokenToUsd( - steps.destinationSwap?.outputAmount || steps.bridge.outputAmount - ) || BigNumber.from(0) + params.convertOutputTokenToUsd(expectedOutputAmount) || BigNumber.from(0) ); + return originSwapFeeUsd.add(destinationSwapFeeUsd); } @@ -832,14 +837,17 @@ export function getTokensForFeesCalc(params: { params.universalSwapQuote.steps.bridge.tokenIn.symbol ) : inputToken; - const outputToken = + const _outputToken = params.isUniversalSwap && params.universalSwapQuote - ? config.getTokenInfoBySymbol( - params.toChainId, - params.universalSwapQuote.steps.destinationSwap?.tokenOut.symbol || - params.universalSwapQuote.steps.bridge.tokenOut.symbol - ) - : params.outputToken; + ? params.universalSwapQuote.outputToken + : { + ...params.outputToken, + chainId: params.toChainId, + }; + const outputToken = config.getTokenInfoBySymbol( + _outputToken.chainId, + _outputToken.symbol + ); return { inputToken, outputToken, diff --git a/src/views/DepositStatus/components/DepositTimesCard.tsx b/src/views/DepositStatus/components/DepositTimesCard.tsx index ad0719aa7..ae1fd80a2 100644 --- a/src/views/DepositStatus/components/DepositTimesCard.tsx +++ b/src/views/DepositStatus/components/DepositTimesCard.tsx @@ -73,7 +73,7 @@ export function DepositTimesCard({ const netFee = estimatedRewards?.netFeeAsBaseCurrency?.toString(); const amountSentBaseCurrency = amountAsBaseCurrency?.toString(); - const { inputToken, bridgeToken } = getTokensForFeesCalc({ + const { inputToken, bridgeToken, outputToken } = getTokensForFeesCalc({ inputToken: getToken(inputTokenSymbol), outputToken: getToken(outputTokenSymbol || inputTokenSymbol), isUniversalSwap: isUniversalSwap, @@ -89,7 +89,7 @@ export function DepositTimesCard({ const { convertTokenToBaseCurrency: convertOutputTokenToUsd, convertBaseCurrencyToToken: convertUsdToOutputToken, - } = useTokenConversion(outputTokenSymbol || inputTokenSymbol, "usd"); + } = useTokenConversion(outputToken.symbol || inputTokenSymbol, "usd"); const { outputAmountUsd } = calcFeesForEstimatedTable({ @@ -262,12 +262,12 @@ function CheckIconExplorerLink({ return ; } + const explorerUrl = chainInfo.intermediaryChain + ? getChainInfo(chainInfo.intermediaryChain).constructExplorerLink(txHash) + : chainInfo.constructExplorerLink(txHash); + return ( - + ); diff --git a/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts b/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts index 673a5b2bc..4dc3c8649 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts @@ -3,7 +3,7 @@ import { getDepositByTxHash, parseFilledRelayLog } from "utils/deposits"; import { getConfig } from "utils/config"; import { getBlockForTimestamp, getMessageHash, toAddressType } from "utils/sdk"; import { NoFilledRelayLogError } from "utils/deposits"; -import { indexerApiBaseUrl } from "utils/constants"; +import { indexerApiBaseUrl, INDIRECT_CHAINS } from "utils/constants"; import axios from "axios"; import { IChainStrategy, @@ -68,6 +68,13 @@ export class EVMStrategy implements IChainStrategy { if (!depositId) { throw new Error("Deposit ID not found in deposit information"); } + + let fillChainId = this.chainId; + + if (INDIRECT_CHAINS[this.chainId]) { + fillChainId = INDIRECT_CHAINS[this.chainId].intermediaryChain; + } + try { // First try the rewards API const { data } = await axios.get<{ @@ -83,7 +90,7 @@ export class EVMStrategy implements IChainStrategy { if (data?.status === "filled" && data.fillTx) { // Get fill transaction details - const provider = getProvider(this.chainId); + const provider = getProvider(fillChainId); const fillTxReceipt = await provider.getTransactionReceipt(data.fillTx); const fillTxBlock = await provider.getBlock(fillTxReceipt.blockNumber); @@ -91,7 +98,7 @@ export class EVMStrategy implements IChainStrategy { if (!parsedFIllLog) { throw new Error( - `Unable to parse FilledRelay logs for tx ${fillTxReceipt.transactionHash} on Chain ${this.chainId}` + `Unable to parse FilledRelay logs for tx ${fillTxReceipt.transactionHash} on Chain ${fillChainId}` ); } @@ -108,7 +115,7 @@ export class EVMStrategy implements IChainStrategy { ), outputToken: toAddressType( parsedFIllLog.args.outputToken, - Number(this.chainId) + Number(fillChainId) ), depositor: toAddressType( parsedFIllLog.args.depositor, @@ -116,17 +123,17 @@ export class EVMStrategy implements IChainStrategy { ), recipient: toAddressType( parsedFIllLog.args.recipient, - Number(this.chainId) + Number(fillChainId) ), exclusiveRelayer: toAddressType( parsedFIllLog.args.exclusiveRelayer, - Number(this.chainId) + Number(fillChainId) ), relayer: toAddressType( parsedFIllLog.args.relayer, - Number(this.chainId) + Number(fillChainId) ), - destinationChainId: this.chainId, + destinationChainId: fillChainId, fillTimestamp: fillTxBlock.timestamp, blockNumber: parsedFIllLog.blockNumber, txnRef: parsedFIllLog.transactionHash, @@ -143,7 +150,7 @@ export class EVMStrategy implements IChainStrategy { ), updatedRecipient: toAddressType( parsedFIllLog.args.relayExecutionInfo.updatedRecipient, - this.chainId + fillChainId ), updatedOutputAmount: parsedFIllLog.args.relayExecutionInfo.updatedOutputAmount, @@ -159,14 +166,14 @@ export class EVMStrategy implements IChainStrategy { // If API approach didn't work, find the fill on-chain try { - const provider = getProvider(this.chainId); + const provider = getProvider(fillChainId); const blockForTimestamp = await getBlockForTimestamp( provider, depositInfo.depositTimestamp ); const config = getConfig(); - const destinationSpokePool = config.getSpokePool(this.chainId); + const destinationSpokePool = config.getSpokePool(fillChainId); const [legacyFilledRelayEvents, newFilledRelayEvents] = await Promise.all( [ destinationSpokePool.queryFilter( @@ -205,7 +212,7 @@ export class EVMStrategy implements IChainStrategy { const filledRelayEvent = filledRelayEvents?.[0]; if (!filledRelayEvent) { - throw new NoFilledRelayLogError(Number(depositId), this.chainId); + throw new NoFilledRelayLogError(Number(depositId), fillChainId); } const messageHash = "messageHash" in filledRelayEvent.args @@ -232,7 +239,7 @@ export class EVMStrategy implements IChainStrategy { ), outputToken: toAddressType( filledRelayEvent.args.outputToken, - Number(this.chainId) + Number(fillChainId) ), depositor: toAddressType( filledRelayEvent.args.depositor, @@ -240,18 +247,18 @@ export class EVMStrategy implements IChainStrategy { ), recipient: toAddressType( filledRelayEvent.args.recipient, - Number(this.chainId) + Number(fillChainId) ), exclusiveRelayer: toAddressType( filledRelayEvent.args.exclusiveRelayer, - Number(this.chainId) + Number(fillChainId) ), relayer: toAddressType( filledRelayEvent.args.relayer, - Number(this.chainId) + Number(fillChainId) ), messageHash, - destinationChainId: this.chainId, + destinationChainId: fillChainId, fillTimestamp: fillTxBlock.timestamp, blockNumber: filledRelayEvent.blockNumber, txnRef: filledRelayEvent.transactionHash, diff --git a/src/views/DepositStatus/hooks/useResolveFromBridgePagePayload.ts b/src/views/DepositStatus/hooks/useResolveFromBridgePagePayload.ts index 8e561b3fc..2523cabd0 100644 --- a/src/views/DepositStatus/hooks/useResolveFromBridgePagePayload.ts +++ b/src/views/DepositStatus/hooks/useResolveFromBridgePagePayload.ts @@ -34,10 +34,9 @@ export function useResolveFromBridgePagePayload( const swapToken = isSwap ? getToken(selectedRoute.swapTokenSymbol) : undefined; - const outputToken = getToken(outputTokenSymbol); - const { inputToken, bridgeToken } = getTokensForFeesCalc({ + const { inputToken, bridgeToken, outputToken } = getTokensForFeesCalc({ inputToken: getToken(inputTokenSymbol), - outputToken, + outputToken: getToken(outputTokenSymbol), isUniversalSwap: !!universalSwapQuote, universalSwapQuote, fromChainId: fromChainId, diff --git a/test/api/_dexes/utils-b2bi.test.ts b/test/api/_dexes/utils-b2bi.test.ts index cc2f2cd0e..a3a15c4c8 100644 --- a/test/api/_dexes/utils-b2bi.test.ts +++ b/test/api/_dexes/utils-b2bi.test.ts @@ -1,8 +1,8 @@ -import { getIndirectDestinationRoutes } from "../../../api/_dexes/utils-b2bi"; +import { getIndirectDestinationRoute } from "../../../api/_dexes/utils-b2bi"; import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "../../../api/_constants"; describe("_dexes/utils-b2bi", () => { - describe("#getIndirectDestinationRoutes()", () => { + describe("#getIndirectDestinationRoute()", () => { test("Optimism USDT -> Arbitrum USDT - should return empty array", () => { const params = { originChainId: CHAIN_IDs.OPTIMISM, @@ -10,8 +10,8 @@ describe("_dexes/utils-b2bi", () => { inputToken: TOKEN_SYMBOLS_MAP.USDT.addresses[CHAIN_IDs.OPTIMISM], outputToken: TOKEN_SYMBOLS_MAP.USDT.addresses[CHAIN_IDs.ARBITRUM], }; - const indirectDestinationRoutes = getIndirectDestinationRoutes(params); - expect(indirectDestinationRoutes).toEqual([]); + const indirectDestinationRoute = getIndirectDestinationRoute(params); + expect(indirectDestinationRoute).toEqual(undefined); }); test("Optimism USDT -> HyperCore USDT - should return indirect destination routes", () => { @@ -22,24 +22,22 @@ describe("_dexes/utils-b2bi", () => { outputToken: TOKEN_SYMBOLS_MAP["USDT-SPOT"].addresses[CHAIN_IDs.HYPERCORE], }; - const indirectDestinationRoutes = getIndirectDestinationRoutes(params); - expect(indirectDestinationRoutes.length).toEqual(1); - expect( - indirectDestinationRoutes[0].intermediaryOutputToken.symbol - ).toEqual("USDT"); - expect( - indirectDestinationRoutes[0].intermediaryOutputToken.chainId - ).toEqual(CHAIN_IDs.HYPEREVM); + const indirectDestinationRoute = getIndirectDestinationRoute(params); + expect(indirectDestinationRoute).toBeTruthy(); + expect(indirectDestinationRoute!.intermediaryOutputToken.symbol).toEqual( + "USDT" + ); + expect(indirectDestinationRoute!.intermediaryOutputToken.chainId).toEqual( + CHAIN_IDs.HYPEREVM + ); expect( - indirectDestinationRoutes[0].intermediaryOutputToken.decimals + indirectDestinationRoute!.intermediaryOutputToken.decimals ).toEqual(6); - expect(indirectDestinationRoutes[0].outputToken.symbol).toEqual( - "USDT-SPOT" - ); - expect(indirectDestinationRoutes[0].outputToken.chainId).toEqual( + expect(indirectDestinationRoute!.outputToken.symbol).toEqual("USDT-SPOT"); + expect(indirectDestinationRoute!.outputToken.chainId).toEqual( CHAIN_IDs.HYPERCORE ); - expect(indirectDestinationRoutes[0].outputToken.decimals).toEqual(8); + expect(indirectDestinationRoute!.outputToken.decimals).toEqual(8); }); test("BSC USDT -> HyperCore USDT - should return indirect destination routes", () => { @@ -50,31 +48,27 @@ describe("_dexes/utils-b2bi", () => { outputToken: TOKEN_SYMBOLS_MAP["USDT-SPOT"].addresses[CHAIN_IDs.HYPERCORE], }; - const indirectDestinationRoutes = getIndirectDestinationRoutes(params); - expect(indirectDestinationRoutes.length).toEqual(1); - expect(indirectDestinationRoutes[0].inputToken.symbol).toEqual( - "USDT-BNB" - ); - expect(indirectDestinationRoutes[0].inputToken.chainId).toEqual( + const indirectDestinationRoute = getIndirectDestinationRoute(params); + expect(indirectDestinationRoute).toBeTruthy(); + expect(indirectDestinationRoute!.inputToken.symbol).toEqual("USDT-BNB"); + expect(indirectDestinationRoute!.inputToken.chainId).toEqual( CHAIN_IDs.BSC ); - expect(indirectDestinationRoutes[0].inputToken.decimals).toEqual(18); - expect( - indirectDestinationRoutes[0].intermediaryOutputToken.symbol - ).toEqual("USDT"); - expect( - indirectDestinationRoutes[0].intermediaryOutputToken.chainId - ).toEqual(CHAIN_IDs.HYPEREVM); + expect(indirectDestinationRoute!.inputToken.decimals).toEqual(18); + expect(indirectDestinationRoute!.intermediaryOutputToken.symbol).toEqual( + "USDT" + ); + expect(indirectDestinationRoute!.intermediaryOutputToken.chainId).toEqual( + CHAIN_IDs.HYPEREVM + ); expect( - indirectDestinationRoutes[0].intermediaryOutputToken.decimals + indirectDestinationRoute!.intermediaryOutputToken.decimals ).toEqual(6); - expect(indirectDestinationRoutes[0].outputToken.symbol).toEqual( - "USDT-SPOT" - ); - expect(indirectDestinationRoutes[0].outputToken.chainId).toEqual( + expect(indirectDestinationRoute!.outputToken.symbol).toEqual("USDT-SPOT"); + expect(indirectDestinationRoute!.outputToken.chainId).toEqual( CHAIN_IDs.HYPERCORE ); - expect(indirectDestinationRoutes[0].outputToken.decimals).toEqual(8); + expect(indirectDestinationRoute!.outputToken.decimals).toEqual(8); }); test("HyperEVM USDT -> HyperCore USDT - should return indirect destination routes", () => { @@ -85,8 +79,8 @@ describe("_dexes/utils-b2bi", () => { outputToken: TOKEN_SYMBOLS_MAP["USDT-SPOT"].addresses[CHAIN_IDs.HYPERCORE], }; - const indirectDestinationRoutes = getIndirectDestinationRoutes(params); - expect(indirectDestinationRoutes).toEqual([]); + const indirectDestinationRoute = getIndirectDestinationRoute(params); + expect(indirectDestinationRoute).toBeFalsy(); }); }); });