From 389f407d2fa1139d45a6604ed1d9b0022950bab2 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 7 Oct 2025 06:40:56 +0200 Subject: [PATCH 1/4] feat: enable USDT hyperevm -> hypercore in FE --- api/swap/_utils.ts | 1 + scripts/generate-swap-routes.ts | 1 - src/data/universal-swap-routes_1.json | 12 ++++++ src/hooks/useBridgeFees.ts | 41 +++++++++++++++++++ src/hooks/useBridgeLimits.ts | 15 ++++++- src/utils/query-keys.ts | 7 +++- .../serverless-api/prod/swap-approval.ts | 12 ++++++ src/views/Bridge/utils.ts | 1 + 8 files changed, 86 insertions(+), 4 deletions(-) diff --git a/api/swap/_utils.ts b/api/swap/_utils.ts index da59fcb71..cb820a548 100644 --- a/api/swap/_utils.ts +++ b/api/swap/_utils.ts @@ -961,6 +961,7 @@ export async function buildBaseSwapResponseJson(params: { tokenIn: params.bridgeQuote.inputToken, tokenOut: params.bridgeQuote.outputToken, fees: params.bridgeQuote.fees, + provider: params.bridgeQuote.provider, }, destinationSwap: params.destinationSwapQuote ? { diff --git a/scripts/generate-swap-routes.ts b/scripts/generate-swap-routes.ts index ca2ca7cf0..99667a3b8 100644 --- a/scripts/generate-swap-routes.ts +++ b/scripts/generate-swap-routes.ts @@ -73,7 +73,6 @@ const enabledSwapRoutes: { }, [TOKEN_SYMBOLS_MAP.USDT.symbol]: { all: { - disabledOriginChains: [CHAIN_IDs.HYPEREVM], enabledDestinationChains: [CHAIN_IDs.HYPERCORE], enabledOutputTokens: ["USDT-SPOT"], }, diff --git a/src/data/universal-swap-routes_1.json b/src/data/universal-swap-routes_1.json index feaba89ce..33d560e65 100644 --- a/src/data/universal-swap-routes_1.json +++ b/src/data/universal-swap-routes_1.json @@ -419,6 +419,18 @@ "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, diff --git a/src/hooks/useBridgeFees.ts b/src/hooks/useBridgeFees.ts index 821a1c828..35b72834d 100644 --- a/src/hooks/useBridgeFees.ts +++ b/src/hooks/useBridgeFees.ts @@ -16,6 +16,41 @@ const DEFAULT_SIMULATED_RECIPIENT_ADDRESS_EVM = const DEFAULT_SIMULATED_RECIPIENT_ADDRESS_SVM = "GsiZqCTNRi4T3qZrixFdmhXVeA4CSUzS7c44EQ7Rw1Tw"; +const EMPTY_BRIDGE_FEES = { + totalRelayFee: { + total: BigNumber.from(0), + pct: BigNumber.from(0), + }, + lpFee: { + total: BigNumber.from(0), + pct: BigNumber.from(0), + }, + relayerGasFee: { + total: BigNumber.from(0), + pct: BigNumber.from(0), + }, + relayerCapitalFee: { + total: BigNumber.from(0), + pct: BigNumber.from(0), + }, + quoteTimestamp: BigNumber.from(0), + quoteTimestampInMs: BigNumber.from(0), + quoteLatency: BigNumber.from(0), + quoteBlock: BigNumber.from(0), + limits: { + maxDepositInstant: BigNumber.from(ethers.constants.MaxUint256), + maxDeposit: BigNumber.from(ethers.constants.MaxUint256), + maxDepositShortDelay: BigNumber.from(ethers.constants.MaxUint256), + minDeposit: BigNumber.from(0), + recommendedDepositInstant: BigNumber.from(ethers.constants.MaxUint256), + }, + estimatedFillTimeSec: 1, + exclusiveRelayer: ethers.constants.AddressZero, + exclusivityDeadline: 0, + fillDeadline: 0, + isAmountTooLow: false, +}; + /** * This hook calculates the bridge fees for a given token and amount. * @param amount - The amount to check bridge fees for. @@ -63,6 +98,7 @@ export function useBridgeFees( bridgeOutputTokenSymbol, bridgeOriginChainId, bridgeDestinationChainId, + didUniversalSwapLoad ? universalSwapQuote.steps.bridge.provider : "across", externalProjectId, recipientAddress ); @@ -76,10 +112,15 @@ export function useBridgeFees( amountToQuery, fromChainIdToQuery, toChainIdToQuery, + bridgeProviderToQuery, externalProjectIdToQuery, recipientAddressToQuery, ] = queryKey; + if (bridgeProviderToQuery === "hypercore") { + return EMPTY_BRIDGE_FEES; + } + const feeArgs = { amount: BigNumber.from(amountToQuery), inputTokenSymbol: inputTokenSymbolToQuery, diff --git a/src/hooks/useBridgeLimits.ts b/src/hooks/useBridgeLimits.ts index d425046a3..904c0deb3 100644 --- a/src/hooks/useBridgeLimits.ts +++ b/src/hooks/useBridgeLimits.ts @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { bridgeLimitsQueryKey, ChainId, getConfig } from "utils"; -import { BigNumber } from "ethers"; +import { BigNumber, ethers } from "ethers"; import getApiEndpoint from "utils/serverless-api"; import { UniversalSwapQuote } from "./useUniversalSwapQuote"; @@ -53,7 +53,8 @@ export function useBridgeLimits( bridgeInputTokenSymbol, bridgeOutputTokenSymbol, bridgeOriginChainId, - bridgeDestinationChainId + bridgeDestinationChainId, + didUniversalSwapLoad ? universalSwapQuote.steps.bridge.provider : "across" ); const { data: limits, ...delegated } = useQuery({ queryKey, @@ -64,6 +65,7 @@ export function useBridgeLimits( outputTokenSymbolToQuery, fromChainIdToQuery, toChainIdToQuery, + bridgeProviderToQuery, ] = queryKey; if ( @@ -75,6 +77,15 @@ export function useBridgeLimits( throw new Error("Bridge limits query not enabled"); } + if (bridgeProviderToQuery === "hypercore") { + return { + minDeposit: BigNumber.from(0), + maxDeposit: BigNumber.from(ethers.constants.MaxUint256), + maxDepositInstant: BigNumber.from(ethers.constants.MaxUint256), + maxDepositShortDelay: BigNumber.from(ethers.constants.MaxUint256), + }; + } + return getApiEndpoint().limits( config.getTokenInfoBySymbol(fromChainIdToQuery, inputTokenSymbolToQuery) .address, diff --git a/src/utils/query-keys.ts b/src/utils/query-keys.ts index 8df885220..3439530b8 100644 --- a/src/utils/query-keys.ts +++ b/src/utils/query-keys.ts @@ -32,6 +32,7 @@ export function balanceQueryKey( * @param amount The amount to check bridge fees for. * @param fromChainId The origin chain of this bridge action * @param toChainId The destination chain of this bridge action + * @param bridgeProvider The bridge provider to check bridge fees for. * @param externalProjectId The external project id to check bridge fees for. * @param recipientAddress The recipient address to check bridge fees for. * @returns An array of query keys for @tanstack/react-query `useQuery` hook. @@ -42,6 +43,7 @@ export function bridgeFeesQueryKey( outputToken: string, fromChainId: ChainId, toChainId: ChainId, + bridgeProvider: string, externalProjectId?: string, recipientAddress?: string ) { @@ -52,6 +54,7 @@ export function bridgeFeesQueryKey( amount.toString(), fromChainId, toChainId, + bridgeProvider, externalProjectId, recipientAddress, ] as const; @@ -61,7 +64,8 @@ export function bridgeLimitsQueryKey( inputToken?: string, outputToken?: string, fromChainId?: ChainId, - toChainId?: ChainId + toChainId?: ChainId, + bridgeProvider?: string ) { return [ "bridgeLimits", @@ -69,6 +73,7 @@ export function bridgeLimitsQueryKey( outputToken, fromChainId, toChainId, + bridgeProvider, ] as const; } diff --git a/src/utils/serverless-api/prod/swap-approval.ts b/src/utils/serverless-api/prod/swap-approval.ts index c3e8f50e7..df24abac2 100644 --- a/src/utils/serverless-api/prod/swap-approval.ts +++ b/src/utils/serverless-api/prod/swap-approval.ts @@ -35,6 +35,10 @@ export type SwapApprovalApiResponse = { }[]; steps: { originSwap?: { + provider: { + name: string; + sources: string[]; + }; tokenIn: SwapApiToken; tokenOut: SwapApiToken; inputAmount: string; @@ -43,6 +47,7 @@ export type SwapApprovalApiResponse = { maxInputAmount: string; }; bridge: { + provider: string; inputAmount: string; outputAmount: string; tokenIn: SwapApiToken; @@ -67,6 +72,10 @@ export type SwapApprovalApiResponse = { }; }; destinationSwap?: { + provider: { + name: string; + sources: string[]; + }; tokenIn: SwapApiToken; tokenOut: SwapApiToken; inputAmount: string; @@ -138,6 +147,7 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { steps: { originSwap: result.steps.originSwap ? { + provider: result.steps.originSwap.provider, tokenIn: result.steps.originSwap.tokenIn, tokenOut: result.steps.originSwap.tokenOut, inputAmount: BigNumber.from(result.steps.originSwap.inputAmount), @@ -151,6 +161,7 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { } : undefined, bridge: { + provider: result.steps.bridge.provider || "across", inputAmount: BigNumber.from(result.steps.bridge.inputAmount), outputAmount: BigNumber.from(result.steps.bridge.outputAmount), tokenIn: result.steps.bridge.tokenIn, @@ -178,6 +189,7 @@ export async function swapApprovalApiCall(params: SwapApprovalApiQueryParams) { }, destinationSwap: result.steps.destinationSwap ? { + provider: result.steps.destinationSwap.provider, tokenIn: result.steps.destinationSwap.tokenIn, tokenOut: result.steps.destinationSwap.tokenOut, inputAmount: BigNumber.from( diff --git a/src/views/Bridge/utils.ts b/src/views/Bridge/utils.ts index d34fe64d2..13f87ba7c 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -23,6 +23,7 @@ import { isStablecoin, ChainId, } from "utils"; +import { ConvertDecimals } from "utils/convertdecimals"; import { SwapQuoteApiResponse } from "utils/serverless-api/prod/swap-quote"; export type SelectedRoute = From 1498025f55ad625295337a6d4bf1aaf0630f0d24 Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Tue, 7 Oct 2025 06:42:41 +0200 Subject: [PATCH 2/4] fixup --- src/utils/serverless-api/mocked/swap-approval.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/serverless-api/mocked/swap-approval.ts b/src/utils/serverless-api/mocked/swap-approval.ts index 6c8ebfae1..32ab3c477 100644 --- a/src/utils/serverless-api/mocked/swap-approval.ts +++ b/src/utils/serverless-api/mocked/swap-approval.ts @@ -38,6 +38,7 @@ export async function swapApprovalApiCall( steps: { originSwap: undefined, bridge: { + provider: "across", inputAmount: BigNumber.from("0"), outputAmount: BigNumber.from("0"), tokenIn: { From 7c5d7df7aa7b3e35d5d05febe192cf07833f05bc Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Thu, 9 Oct 2025 07:35:32 +0200 Subject: [PATCH 3/4] fixup --- src/views/Bridge/utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/views/Bridge/utils.ts b/src/views/Bridge/utils.ts index 13f87ba7c..d34fe64d2 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -23,7 +23,6 @@ import { isStablecoin, ChainId, } from "utils"; -import { ConvertDecimals } from "utils/convertdecimals"; import { SwapQuoteApiResponse } from "utils/serverless-api/prod/swap-quote"; export type SelectedRoute = From bcdd83d303ad9cf99804b199b1152618bc733eef Mon Sep 17 00:00:00 2001 From: Dong-Ha Kim Date: Thu, 9 Oct 2025 09:32:44 +0200 Subject: [PATCH 4/4] fix: deposit status page --- src/utils/hyperevm.ts | 133 ++++++++++++++++++ src/utils/typechain.ts | 1 + .../DepositStatus/hooks/useDepositTracking.ts | 10 +- .../useDepositTracking/strategies/evm.ts | 26 +++- .../hooks/useDepositTracking/types.ts | 11 +- 5 files changed, 172 insertions(+), 9 deletions(-) create mode 100644 src/utils/hyperevm.ts diff --git a/src/utils/hyperevm.ts b/src/utils/hyperevm.ts new file mode 100644 index 000000000..721c495e6 --- /dev/null +++ b/src/utils/hyperevm.ts @@ -0,0 +1,133 @@ +import { ethers, BigNumber } from "ethers"; + +import { ChainId } from "./constants"; +import { getDepositByTxHash } from "./deposits"; +import { getProvider } from "./providers"; +import { ERC20__factory, TransferEvent } from "./typechain"; +import { toAddressType } from "./sdk"; + +import type { + DepositData, + DepositedInfo, + FillInfo, +} from "views/DepositStatus/hooks/useDepositTracking/types"; + +export async function getDepositByTxHashToHyperCore( + txHash: string, + originChainId: number +): ReturnType { + if (originChainId !== ChainId.HYPEREVM) { + throw new Error( + `Could not fetch HyperEVM deposit for origin chain ${originChainId}` + ); + } + const destinationChainId = ChainId.HYPERCORE; + + const provider = getProvider(originChainId); + const depositTxReceipt = await provider.getTransactionReceipt(txHash); + if (!depositTxReceipt) { + throw new Error( + `Could not fetch tx receipt for ${txHash} on chain ${originChainId}` + ); + } + + const block = await provider.getBlock(depositTxReceipt.blockNumber); + + if (depositTxReceipt.status === 0) { + return { + depositTxReceipt, + parsedDepositLog: undefined, + depositTimestamp: block.timestamp, + }; + } + + const transferLog = parseTransferLog(depositTxReceipt.logs); + if (!transferLog) { + throw new Error(`Could not parse 'Transfer' log for ${txHash}`); + } + + // Convert HyperEVM deposit log to Across deposit log + const parsedDepositLog: DepositData = { + depositId: BigNumber.from(0), + originChainId, + destinationChainId, + depositor: toAddressType(transferLog.args.from, originChainId), + // HyperCore recipient must be the HyperEVM depositor in initial Swap API support + recipient: toAddressType(transferLog.args.from, destinationChainId), + exclusiveRelayer: toAddressType( + ethers.constants.AddressZero, + destinationChainId + ), + inputToken: toAddressType(transferLog.address, originChainId), + // HyperCore output token is the system address on HyperEVM, i.e. to address of Transfer log + outputToken: toAddressType(transferLog.args.to, destinationChainId), + inputAmount: transferLog.args.value, + outputAmount: transferLog.args.value, + quoteTimestamp: block.timestamp, + fillDeadline: block.timestamp, + depositTimestamp: block.timestamp, + messageHash: "0x", + message: "0x", + exclusivityDeadline: 0, + blockNumber: depositTxReceipt.blockNumber, + txnIndex: depositTxReceipt.transactionIndex, + logIndex: transferLog.logIndex, + txnRef: depositTxReceipt.transactionHash, + }; + + return { + depositTxReceipt, + parsedDepositLog, + depositTimestamp: block.timestamp, + }; +} + +export async function getFillForDepositToHyperCore( + depositOnHyperEVM: DepositedInfo +): Promise { + // TODO: Use HyperCore API to check if bridge succeeded + return { + fillTxHash: depositOnHyperEVM.depositLog.txnRef, + fillTxTimestamp: depositOnHyperEVM.depositTimestamp, + depositInfo: depositOnHyperEVM, + status: "filled", + fillLog: { + ...depositOnHyperEVM.depositLog, + fillTimestamp: depositOnHyperEVM.depositTimestamp, + relayer: depositOnHyperEVM.depositLog.depositor, + repaymentChainId: depositOnHyperEVM.depositLog.originChainId, + relayExecutionInfo: { + updatedRecipient: depositOnHyperEVM.depositLog.recipient, + updatedOutputAmount: depositOnHyperEVM.depositLog.outputAmount, + updatedMessageHash: depositOnHyperEVM.depositLog.messageHash, + fillType: 0, // FastFill + }, + }, + }; +} + +function parseTransferLog( + logs: Array<{ + topics: string[]; + data: string; + }> +) { + const erc20Iface = ERC20__factory.createInterface(); + const parsedLogs = logs.flatMap((log) => { + try { + console.log("log", log); + const parsedLog = erc20Iface.parseLog(log); + return parsedLog.name === "Transfer" + ? { + ...log, + ...parsedLog, + } + : []; + } catch (e) { + return []; + } + }); + return parsedLogs.find(({ name }) => name === "Transfer") as unknown as + | TransferEvent + | undefined; +} diff --git a/src/utils/typechain.ts b/src/utils/typechain.ts index fef94a435..d25f8da06 100644 --- a/src/utils/typechain.ts +++ b/src/utils/typechain.ts @@ -33,3 +33,4 @@ export type { TypedEvent, TypedEventFilter, } from "@across-protocol/contracts/dist/typechain/common"; +export type { TransferEvent } from "@across-protocol/contracts/dist/typechain/@openzeppelin/contracts/token/ERC20/ERC20"; diff --git a/src/views/DepositStatus/hooks/useDepositTracking.ts b/src/views/DepositStatus/hooks/useDepositTracking.ts index ad0e3a710..90ae92381 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking.ts @@ -54,6 +54,9 @@ export function useDepositTracking({ const account = getEcosystem(fromChainId) === "evm" ? accountEVM : accountSVM?.toBase58(); + const { universalSwapQuote } = fromBridgePagePayload || {}; + const bridgeProvider = universalSwapQuote?.steps.bridge.provider || "across"; + // Create appropriate strategy for the source chain const { depositStrategy, fillStrategy } = useMemo( () => createChainStrategies(fromChainId, toChainId), @@ -62,14 +65,14 @@ export function useDepositTracking({ // Query for deposit information const depositQuery = useQuery({ - queryKey: ["deposit", depositTxHash, fromChainId, account], + queryKey: ["deposit", depositTxHash, fromChainId, account, bridgeProvider], queryFn: async () => { // On some L2s the tx is mined too fast for the animation to show, so we add a delay await wait(1_000); try { // Use the strategy to get deposit information through the normalized interface - return depositStrategy.getDeposit(depositTxHash); + return depositStrategy.getDeposit(depositTxHash, bridgeProvider); } catch (e) { // Don't retry if the deposit doesn't exist or is invalid if (e instanceof NoFundsDepositedLogError) { @@ -146,6 +149,7 @@ export function useDepositTracking({ depositTxHash, fromChainId, toChainId, + bridgeProvider, ], queryFn: async () => { const depositInfo = depositQuery.data; @@ -155,7 +159,7 @@ export function useDepositTracking({ } logRelayData(depositInfo.depositLog); // Use the strategy to get fill information through the normalized interface - return await fillStrategy.getFill(depositInfo); + return await fillStrategy.getFill(depositInfo, bridgeProvider); }, staleTime: Infinity, retry: true, diff --git a/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts b/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts index 4dc3c8649..67fb23f04 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking/strategies/evm.ts @@ -1,5 +1,9 @@ import { getProvider } from "utils/providers"; import { getDepositByTxHash, parseFilledRelayLog } from "utils/deposits"; +import { + getDepositByTxHashToHyperCore, + getFillForDepositToHyperCore, +} from "utils/hyperevm"; import { getConfig } from "utils/config"; import { getBlockForTimestamp, getMessageHash, toAddressType } from "utils/sdk"; import { NoFilledRelayLogError } from "utils/deposits"; @@ -26,11 +30,18 @@ export class EVMStrategy implements IChainStrategy { /** * Get deposit information from an EVM transaction hash * @param txHash EVM transaction hash + * @param bridgeProvider Bridge provider * @returns Deposit information */ - async getDeposit(txHash: string): Promise { + async getDeposit( + txHash: string, + bridgeProvider = "across" + ): Promise { try { - const deposit = await getDepositByTxHash(txHash, this.chainId); + const deposit = + bridgeProvider === "across" + ? await getDepositByTxHash(txHash, this.chainId) + : await getDepositByTxHashToHyperCore(txHash, this.chainId); // Create a normalized response if (!deposit.depositTimestamp || !deposit.parsedDepositLog) { @@ -59,16 +70,23 @@ export class EVMStrategy implements IChainStrategy { /** * Get fill information for a deposit * @param depositInfo Deposit information - * @param toChainId Destination chain ID + * @param bridgeProvider Bridge provider * @returns Fill information */ - async getFill(depositInfo: DepositedInfo): Promise { + async getFill( + depositInfo: DepositedInfo, + bridgeProvider = "across" + ): Promise { const depositId = depositInfo.depositLog?.depositId; const originChainId = depositInfo.depositLog.originChainId; if (!depositId) { throw new Error("Deposit ID not found in deposit information"); } + if (bridgeProvider === "hypercore") { + return getFillForDepositToHyperCore(depositInfo); + } + let fillChainId = this.chainId; if (INDIRECT_CHAINS[this.chainId]) { diff --git a/src/views/DepositStatus/hooks/useDepositTracking/types.ts b/src/views/DepositStatus/hooks/useDepositTracking/types.ts index d5d0fea0b..80d13a6d7 100644 --- a/src/views/DepositStatus/hooks/useDepositTracking/types.ts +++ b/src/views/DepositStatus/hooks/useDepositTracking/types.ts @@ -66,9 +66,13 @@ export interface IChainStrategy { /** * Get deposit information from a transaction * @param txIdOrSignature Transaction hash or signature + * @param bridgeProvider Bridge provider * @returns Normalized deposit information */ - getDeposit(txIdOrSignature: string): Promise; + getDeposit( + txIdOrSignature: string, + bridgeProvider?: string + ): Promise; /** * Get fill information for a deposit @@ -76,7 +80,10 @@ export interface IChainStrategy { * @param toChainId Destination chain ID * @returns Normalized fill information */ - getFill(depositInfo: DepositedInfo): Promise; + getFill( + depositInfo: DepositedInfo, + bridgeProvider?: string + ): Promise; /** * Convert deposit information to local storage format