From a0b61844c5940ff3dbf13a2c928456c932a647e8 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 25 Sep 2025 13:56:19 +0200 Subject: [PATCH 01/18] generate swap routes to hypercore Signed-off-by: Gerhard Steenkamp --- scripts/generate-swap-routes.ts | 9 +- src/data/universal-swap-routes_1.json | 144 ++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 5 deletions(-) diff --git a/scripts/generate-swap-routes.ts b/scripts/generate-swap-routes.ts index 78aad575b..25f0467a8 100644 --- a/scripts/generate-swap-routes.ts +++ b/scripts/generate-swap-routes.ts @@ -72,11 +72,10 @@ 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: { + enabledDestinationChains: [CHAIN_IDs.HYPERCORE], + enabledOutputTokens: ["USDT-SPOT"], + }, [CHAIN_IDs.MAINNET]: { enabledDestinationChains: [CHAIN_IDs.LENS], enabledOutputTokens: ["GHO"], diff --git a/src/data/universal-swap-routes_1.json b/src/data/universal-swap-routes_1.json index ec3b012a7..3d53e9d49 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": 999, + "toChain": 1337, + "fromTokenAddress": "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", + "toTokenAddress": "0x200000000000000000000000000000000000010C", + "fromTokenSymbol": "USDT", + "toTokenSymbol": "USDT-SPOT", + "fromSpokeAddress": "0x35E63eA3eb0fb7A3bc543C71FB66412e1F6B0E04", + "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": 1, "toChain": 232, From 8b20af8894c7ff2ae018a5f6c2bffba1bfaad247 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 25 Sep 2025 16:57:29 +0200 Subject: [PATCH 02/18] bump packages Signed-off-by: Gerhard Steenkamp --- package.json | 2 +- yarn.lock | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b97822fe2..670b1abde 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "license": "AGPL-3.0-only", "dependencies": { - "@across-protocol/constants": "^3.1.77", + "@across-protocol/constants": "^3.1.80", "@across-protocol/contracts": "^4.1.9", "@across-protocol/contracts-v4.1.1": "npm:@across-protocol/contracts@4.1.1", "@across-protocol/sdk": "^4.3.67", diff --git a/yarn.lock b/yarn.lock index 4efb46e75..0243f8537 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,6 +26,11 @@ resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.79.tgz#674363af1e5ee177ad8962c7dec075b21c14e222" integrity sha512-7CnAkskCXCcEWNJrOC6xDzO6bfJXdNrzVfNsBsVvUGMPBn144yuBYwnLb0uIaYdZB6fX1O8bHqWsLxPxVTMx8A== +"@across-protocol/constants@^3.1.80": + version "3.1.80" + resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.80.tgz#a1515f9c8ca19a5a7c2e709da08c1d1f3ac01b28" + integrity sha512-/MtvKygLNoxTFAIOU6FmR4TeEeueL1hyoWit9BtL0RVyXZ1h3zvNr58TYxcoXIFF8Vb+yizyACX32b+bdk7fGg== + "@across-protocol/contracts-v4.1.1@npm:@across-protocol/contracts@4.1.1": version "4.1.1" resolved "https://registry.yarnpkg.com/@across-protocol/contracts/-/contracts-4.1.1.tgz#91c8e0fc867911a17f21b2d79d586f95417cb912" From 9a7bd0ad15791a0e300c21ac41554a242a935ebb Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Thu, 25 Sep 2025 17:29:56 +0200 Subject: [PATCH 03/18] show universal swap routes in chain selector Signed-off-by: Gerhard Steenkamp --- src/views/Bridge/utils.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/views/Bridge/utils.ts b/src/views/Bridge/utils.ts index 8b5a3897d..976f1cdd7 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -530,21 +530,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; } From c77097b80d74eebd55fa93934537ab2fd56027bd Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 26 Sep 2025 12:12:03 +0200 Subject: [PATCH 04/18] fixup --- src/constants/tokens.ts | 1 + 1 file changed, 1 insertion(+) 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, From 1fe942efccfecd1f4bacdb8726bcf5b99ccd52d7 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Fri, 26 Sep 2025 12:28:37 +0200 Subject: [PATCH 05/18] fix: fees and limits query --- src/hooks/useBridgeFees.ts | 10 ++++++++-- src/hooks/useBridgeLimits.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) 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, From ebb0d8ffcda7b371aee82e16e89cfcc0ba762e82 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 29 Sep 2025 04:52:56 +0200 Subject: [PATCH 06/18] fix: output token resolution --- src/utils/constants.ts | 3 +++ .../Bridge/components/FeesCollapsible.tsx | 4 ++-- src/views/Bridge/utils.ts | 18 +++++++++++------- .../components/DepositTimesCard.tsx | 4 ++-- .../hooks/useResolveFromBridgePagePayload.ts | 5 ++--- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 9f21d0030..9e7af5309 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -106,6 +106,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/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/utils.ts b/src/views/Bridge/utils.ts index 976f1cdd7..0ff604daa 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -833,14 +833,18 @@ 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.steps.destinationSwap?.tokenOut || + params.universalSwapQuote.steps.bridge.tokenOut + : { + ...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..f6c84dbb7 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({ 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, From 8d6fd836c162be905279caded378bc2cd8fc6fc8 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 29 Sep 2025 04:57:44 +0200 Subject: [PATCH 07/18] dedup constants in lock --- yarn.lock | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/yarn.lock b/yarn.lock index 0243f8537..dd367ba6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16,17 +16,7 @@ "@uma/common" "^2.17.0" hardhat "^2.9.3" -"@across-protocol/constants@^3.1.69", "@across-protocol/constants@^3.1.77": - version "3.1.77" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.77.tgz#0d57db8676a7eaf4541b2cc9143d95e8d753c0af" - integrity sha512-zRRTFAKhLgAA1QBJsbG5KautFczW0NV0N2hPW8dIn3cyC+pvYBU8FBb3gQ2Vp9b++kg1Gk1rSpMVMKKnjUBtOA== - -"@across-protocol/constants@^3.1.78": - version "3.1.79" - resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.79.tgz#674363af1e5ee177ad8962c7dec075b21c14e222" - integrity sha512-7CnAkskCXCcEWNJrOC6xDzO6bfJXdNrzVfNsBsVvUGMPBn144yuBYwnLb0uIaYdZB6fX1O8bHqWsLxPxVTMx8A== - -"@across-protocol/constants@^3.1.80": +"@across-protocol/constants@^3.1.69", "@across-protocol/constants@^3.1.77", "@across-protocol/constants@^3.1.78", "@across-protocol/constants@^3.1.80": version "3.1.80" resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.80.tgz#a1515f9c8ca19a5a7c2e709da08c1d1f3ac01b28" integrity sha512-/MtvKygLNoxTFAIOU6FmR4TeEeueL1hyoWit9BtL0RVyXZ1h3zvNr58TYxcoXIFF8Vb+yizyACX32b+bdk7fGg== From eb5fdfee36a00887e8214cbd5e1b68668e3ff797 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 29 Sep 2025 05:34:44 +0200 Subject: [PATCH 08/18] fix: output amount calculation --- src/utils/serverless-api/prod/swap-approval.ts | 4 ++++ src/views/Bridge/utils.ts | 10 ++++------ 2 files changed, 8 insertions(+), 6 deletions(-) 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/utils.ts b/src/views/Bridge/utils.ts index 0ff604daa..58b3bf0f3 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -775,7 +775,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( @@ -786,10 +786,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); } @@ -835,8 +834,7 @@ export function getTokensForFeesCalc(params: { : inputToken; const _outputToken = params.isUniversalSwap && params.universalSwapQuote - ? params.universalSwapQuote.steps.destinationSwap?.tokenOut || - params.universalSwapQuote.steps.bridge.tokenOut + ? params.universalSwapQuote.outputToken : { ...params.outputToken, chainId: params.toChainId, From e5cd9245aca0f07880df761dd3d3c4462663b37e Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 29 Sep 2025 08:51:39 +0200 Subject: [PATCH 09/18] fixup --- src/utils/serverless-api/mocked/swap-approval.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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), From a96465d9fe6ce8ef53ef393f240a4a34dc66fbaa Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 29 Sep 2025 09:29:11 +0200 Subject: [PATCH 10/18] fix: fill status tracking hypercore --- scripts/chain-configs/hypercore/index.ts | 6 +-- scripts/chain-configs/types.ts | 16 +------ scripts/generate-routes.ts | 44 ++++++++----------- src/data/indirect_chains_1.json | 12 ++++- src/utils/constants.ts | 9 ++++ .../useDepositTracking/strategies/evm.ts | 41 ++++++++++------- 6 files changed, 64 insertions(+), 64 deletions(-) diff --git a/scripts/chain-configs/hypercore/index.ts b/scripts/chain-configs/hypercore/index.ts index 2361b9617..f06ab9c0b 100644 --- a/scripts/chain-configs/hypercore/index.ts +++ b/scripts/chain-configs/hypercore/index.ts @@ -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/generate-routes.ts b/scripts/generate-routes.ts index 64db652b3..47aff2450 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -208,8 +208,7 @@ const enabledRoutes = { }, routes: transformChainConfigs( enabledMainnetChainConfigs, - enabledMainnetExternalProjects, - enabledIndirectMainnetChainConfigs + enabledMainnetExternalProjects ), }, [CHAIN_IDs.SEPOLIA]: { @@ -242,18 +241,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; @@ -724,7 +718,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`; @@ -749,7 +743,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") { @@ -767,25 +761,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/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/utils/constants.ts b/src/utils/constants.ts index 9e7af5309..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, 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, From 42eb9d67660db412b81e830d0f6ce78db1bccb14 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 29 Sep 2025 09:40:29 +0200 Subject: [PATCH 11/18] fix: api types --- api/_dexes/cross-swap-service.ts | 14 +-- api/_dexes/utils-b2bi.ts | 179 +++++++++++++---------------- test/api/_dexes/utils-b2bi.test.ts | 72 ++++++------ 3 files changed, 120 insertions(+), 145 deletions(-) diff --git a/api/_dexes/cross-swap-service.ts b/api/_dexes/cross-swap-service.ts index a14a71c49..fa457383f 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, @@ -225,21 +225,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( @@ -299,21 +297,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/test/api/_dexes/utils-b2bi.test.ts b/test/api/_dexes/utils-b2bi.test.ts index cc2f2cd0e..1fb0a8e05 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).toBeTruthy(); }); }); }); From 4c195c8815fb0b17f5ee8c12b283553e5f9b3551 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Mon, 29 Sep 2025 09:50:16 +0200 Subject: [PATCH 12/18] fixup --- test/api/_dexes/utils-b2bi.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/api/_dexes/utils-b2bi.test.ts b/test/api/_dexes/utils-b2bi.test.ts index 1fb0a8e05..a3a15c4c8 100644 --- a/test/api/_dexes/utils-b2bi.test.ts +++ b/test/api/_dexes/utils-b2bi.test.ts @@ -80,7 +80,7 @@ describe("_dexes/utils-b2bi", () => { TOKEN_SYMBOLS_MAP["USDT-SPOT"].addresses[CHAIN_IDs.HYPERCORE], }; const indirectDestinationRoute = getIndirectDestinationRoute(params); - expect(indirectDestinationRoute).toBeTruthy(); + expect(indirectDestinationRoute).toBeFalsy(); }); }); }); From d0a421c35aa636566ea638679b3b9f9f93936955 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 30 Sep 2025 10:32:10 +0200 Subject: [PATCH 13/18] chore: distinguished label for Spots and Perps --- scripts/chain-configs/hypercore/index.ts | 2 +- scripts/extern-configs/hyperliquid/index.ts | 1 + src/constants/chains/configs.ts | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/chain-configs/hypercore/index.ts b/scripts/chain-configs/hypercore/index.ts index f06ab9c0b..93449990a 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 (Spot)", logoPath: "./assets/logo.svg", grayscaleLogoPath: "./assets/grayscale-logo.svg", spokePool: { diff --git a/scripts/extern-configs/hyperliquid/index.ts b/scripts/extern-configs/hyperliquid/index.ts index 30bf63c0d..2cc5612cb 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 (Perps)", projectId: "hyperliquid", explorer: "https://arbiscan.io", logoPath: "./assets/logo.svg", diff --git a/src/constants/chains/configs.ts b/src/constants/chains/configs.ts index 720ecd35b..4641ef702 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 (Spot)", chainId: 1337, logoURI: hyperCoreLogo, grayscaleLogoURI: hyperCoreGrayscaleLogo, @@ -1516,7 +1516,7 @@ export const zora_viem = defineChain({ export const hyperliquid = { name: "Hyperliquid", - fullName: "Hyperliquid", + fullName: "Hyperliquid (Perps)", projectId: "hyperliquid", logoURI: hyperliquidLogo, grayscaleLogoURI: hyperliquidGrayscaleLogo, From d55d9b08fbe44256d61cdf6bfae46f5910f08615 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Wed, 1 Oct 2025 04:37:01 +0200 Subject: [PATCH 14/18] fix: merge hypercore via arbitrum and hyperevm --- scripts/chain-configs/hypercore/index.ts | 2 +- scripts/extern-configs/hyperliquid/index.ts | 2 +- scripts/generate-swap-routes.ts | 7 ++++++ scripts/generate-ui-assets.ts | 1 + src/constants/chains/configs.ts | 5 ++-- src/constants/chains/index.ts | 4 +++- src/data/universal-swap-routes_1.json | 12 +++++----- src/views/Bridge/components/ChainSelector.tsx | 21 ++++++++++++---- src/views/Bridge/hooks/useSelectRoute.ts | 24 ++++++++++++++----- src/views/Bridge/utils.ts | 13 ++++++---- .../components/DepositTimesCard.tsx | 10 ++++---- 11 files changed, 70 insertions(+), 31 deletions(-) diff --git a/scripts/chain-configs/hypercore/index.ts b/scripts/chain-configs/hypercore/index.ts index 93449990a..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: "Hyperliquid (Spot)", + fullName: "Hyperliquid", logoPath: "./assets/logo.svg", grayscaleLogoPath: "./assets/grayscale-logo.svg", spokePool: { diff --git a/scripts/extern-configs/hyperliquid/index.ts b/scripts/extern-configs/hyperliquid/index.ts index 2cc5612cb..e33bb5e38 100644 --- a/scripts/extern-configs/hyperliquid/index.ts +++ b/scripts/extern-configs/hyperliquid/index.ts @@ -3,7 +3,7 @@ import { ExternalProjectConfig } from "../types"; export default { name: "Hyperliquid", - fullName: "Hyperliquid (Perps)", + fullName: "Hyperliquid", projectId: "hyperliquid", explorer: "https://arbiscan.io", logoPath: "./assets/logo.svg", diff --git a/scripts/generate-swap-routes.ts b/scripts/generate-swap-routes.ts index 25f0467a8..ca2ca7cf0 100644 --- a/scripts/generate-swap-routes.ts +++ b/scripts/generate-swap-routes.ts @@ -73,6 +73,7 @@ const enabledSwapRoutes: { }, [TOKEN_SYMBOLS_MAP.USDT.symbol]: { all: { + disabledOriginChains: [CHAIN_IDs.HYPEREVM], enabledDestinationChains: [CHAIN_IDs.HYPERCORE], enabledOutputTokens: ["USDT-SPOT"], }, @@ -81,6 +82,12 @@ const enabledSwapRoutes: { 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/constants/chains/configs.ts b/src/constants/chains/configs.ts index 4641ef702..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: "Hyperliquid (Spot)", + 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 = { @@ -1516,7 +1517,7 @@ export const zora_viem = defineChain({ export const hyperliquid = { name: "Hyperliquid", - fullName: "Hyperliquid (Perps)", + fullName: "Hyperliquid", projectId: "hyperliquid", logoURI: hyperliquidLogo, grayscaleLogoURI: hyperliquidGrayscaleLogo, 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/data/universal-swap-routes_1.json b/src/data/universal-swap-routes_1.json index 3d53e9d49..feaba89ce 100644 --- a/src/data/universal-swap-routes_1.json +++ b/src/data/universal-swap-routes_1.json @@ -420,25 +420,25 @@ "isNative": false }, { - "fromChain": 999, + "fromChain": 9745, "toChain": 1337, "fromTokenAddress": "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", "toTokenAddress": "0x200000000000000000000000000000000000010C", "fromTokenSymbol": "USDT", "toTokenSymbol": "USDT-SPOT", - "fromSpokeAddress": "0x35E63eA3eb0fb7A3bc543C71FB66412e1F6B0E04", + "fromSpokeAddress": "0x50039fAEfebef707cFD94D6d462fE6D10B39207a", "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "type": "universal-swap", "isNative": false }, { - "fromChain": 9745, + "fromChain": 56, "toChain": 1337, - "fromTokenAddress": "0xB8CE59FC3717ada4C02eaDF9682A9e934F625ebb", + "fromTokenAddress": "0x55d398326f99059fF775485246999027B3197955", "toTokenAddress": "0x200000000000000000000000000000000000010C", - "fromTokenSymbol": "USDT", + "fromTokenSymbol": "USDT-BNB", "toTokenSymbol": "USDT-SPOT", - "fromSpokeAddress": "0x50039fAEfebef707cFD94D6d462fE6D10B39207a", + "fromSpokeAddress": "0x4e8E101924eDE233C13e2D8622DC8aED2872d505", "l1TokenAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7", "type": "universal-swap", "isNative": false diff --git a/src/views/Bridge/components/ChainSelector.tsx b/src/views/Bridge/components/ChainSelector.tsx index 5cc577907..fa470cd6c 100644 --- a/src/views/Bridge/components/ChainSelector.tsx +++ b/src/views/Bridge/components/ChainSelector.tsx @@ -4,8 +4,8 @@ import { Selector } from "components"; import { Text } from "components/Text"; import { + ChainId, ChainInfo, - Route, capitalizeFirstLetter, getChainInfo, getToken, @@ -18,11 +18,11 @@ import { useConnectionSVM } from "hooks/useConnectionSVM"; import { useBalanceBySymbolPerChain, zeroBalance } from "hooks/useBalance"; import { useMemo } from "react"; import { BigNumber } from "ethers"; -import { getSupportedChains, findEnabledRoute } from "../utils"; +import { getSupportedChains, findEnabledRoute, SelectedRoute } from "../utils"; import { externConfigs } from "constants/chains/configs"; type Props = { - selectedRoute: Route; + selectedRoute: SelectedRoute; fromOrTo: "from" | "to"; toAddress?: string; onSelectChain: (chainId: number, externalProjectId?: string) => 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/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 58b3bf0f3..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( diff --git a/src/views/DepositStatus/components/DepositTimesCard.tsx b/src/views/DepositStatus/components/DepositTimesCard.tsx index f6c84dbb7..ae1fd80a2 100644 --- a/src/views/DepositStatus/components/DepositTimesCard.tsx +++ b/src/views/DepositStatus/components/DepositTimesCard.tsx @@ -262,12 +262,12 @@ function CheckIconExplorerLink({ return ; } + const explorerUrl = chainInfo.intermediaryChain + ? getChainInfo(chainInfo.intermediaryChain).constructExplorerLink(txHash) + : chainInfo.constructExplorerLink(txHash); + return ( - + ); From b638e747763bf28976171eee61daa904fe6a6efd Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Wed, 1 Oct 2025 11:54:37 +0200 Subject: [PATCH 15/18] fixup --- api/coingecko.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/coingecko.ts b/api/coingecko.ts index d85bf65bb..6901d6706 100644 --- a/api/coingecko.ts +++ b/api/coingecko.ts @@ -289,8 +289,8 @@ async function resolvePriceBySymbol(params: { } const symbol = String( - redirectedLookupSymbols[_symbol.toLowerCase()] ?? - TOKEN_EQUIVALENCE_REMAPPING[_symbol.toLowerCase()] ?? + redirectedLookupSymbols[_symbol.toUpperCase()] ?? + TOKEN_EQUIVALENCE_REMAPPING[_symbol.toUpperCase()] ?? _symbol ).toUpperCase(); From 37b534e1d68b399d6be7cd5982479451f7d8f466 Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 1 Oct 2025 18:20:10 +0200 Subject: [PATCH 16/18] show warning if account not initialized Signed-off-by: Gerhard Steenkamp --- src/hooks/useMustInitializeHyperliquid.ts | 41 ++++++++++++++++++++++ src/utils/hyperliquid.ts | 22 ++++++++++++ src/views/Bridge/Bridge.tsx | 2 ++ src/views/Bridge/components/BridgeForm.tsx | 19 ++++++++++ src/views/Bridge/hooks/useBridge.ts | 9 +++++ 5 files changed, 93 insertions(+) create mode 100644 src/hooks/useMustInitializeHyperliquid.ts 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/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/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..7b57d7b21 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,17 @@ const BridgeForm = ({ )} + {showHyperliquidWarning && ( + + + Account Required + + You must initialize an account for this recipient address on + Hyperliquid before bridging. + + + + )} Date: Wed, 1 Oct 2025 18:21:55 +0200 Subject: [PATCH 17/18] fixup Signed-off-by: Gerhard Steenkamp --- src/views/Bridge/hooks/useBridge.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/views/Bridge/hooks/useBridge.ts b/src/views/Bridge/hooks/useBridge.ts index 3dda6bab4..3bdc464aa 100644 --- a/src/views/Bridge/hooks/useBridge.ts +++ b/src/views/Bridge/hooks/useBridge.ts @@ -84,8 +84,6 @@ export function useBridge() { route: selectedRoute, }); - console.log("showHyperliquidWarning", showHyperliquidWarning); - const { quotedFees, quotedSwap, From 6ba4eb44fae278cf9567bd547affd96b74e9a8fd Mon Sep 17 00:00:00 2001 From: Gerhard Steenkamp Date: Wed, 1 Oct 2025 19:07:07 +0200 Subject: [PATCH 18/18] link to USDC route Signed-off-by: Gerhard Steenkamp --- src/views/Bridge/components/BridgeForm.tsx | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/views/Bridge/components/BridgeForm.tsx b/src/views/Bridge/components/BridgeForm.tsx index 7b57d7b21..9174ce6c2 100644 --- a/src/views/Bridge/components/BridgeForm.tsx +++ b/src/views/Bridge/components/BridgeForm.tsx @@ -16,6 +16,7 @@ import { rewardProgramsAvailable, COLORS, getEcosystem, + getBridgeUrlWithQueryParams, } from "utils"; import { VoidHandler } from "utils/types"; import { SwapQuoteApiResponse } from "utils/serverless-api/prod/swap-quote"; @@ -39,6 +40,7 @@ import { getReceiveTokenSymbol, } from "../utils"; import { ToAccount } from "../hooks/useToAccount"; +import { CHAIN_IDs, TOKEN_SYMBOLS_MAP } from "@across-protocol/constants"; export type BridgeFormProps = { selectedRoute: SelectedRoute; @@ -281,7 +283,17 @@ const BridgeForm = ({ Account Required You must initialize an account for this recipient address on - Hyperliquid before bridging. + Hyperliquid before bridging.{" "} + + Bridge USDC to Hyperliquid + @@ -346,6 +358,16 @@ const BridgeForm = ({ export default BridgeForm; +const InternalLink = styled.a` + color: inherit; + display: inline; + transition: opacity 150ms ease-in-out; + + &:hover { + opacity: 0.8; + } +`; + const CardWrapper = styled(ExternalCardWrapper)` width: 100%; `;