From a070662f05eb2a3b88736530300c1fd0f59ee626 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Wed, 8 Jan 2025 09:21:49 -0500 Subject: [PATCH 1/9] feat(hyperliquid): begin UI interface Signed-off-by: james-a-morris --- .../hyper-liquid/assets/grayscale-logo.svg | 5 + .../hyper-liquid/assets/logo.svg | 5 + scripts/extern-configs/hyper-liquid/index.ts | 13 + scripts/extern-configs/index.ts | 1 + scripts/extern-configs/types.ts | 13 + scripts/generate-routes.ts | 285 +++++++++++------- scripts/generate-ui-assets.ts | 67 +++- .../extern-logos/hyper-liquid-grayscale.svg | 5 + src/assets/extern-logos/hyper-liquid.svg | 5 + src/constants/chains/configs.ts | 26 ++ ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 72 +++++ src/utils/constants.ts | 1 + src/views/Bridge/components/ChainSelector.tsx | 29 +- 13 files changed, 410 insertions(+), 117 deletions(-) create mode 100644 scripts/extern-configs/hyper-liquid/assets/grayscale-logo.svg create mode 100644 scripts/extern-configs/hyper-liquid/assets/logo.svg create mode 100644 scripts/extern-configs/hyper-liquid/index.ts create mode 100644 scripts/extern-configs/index.ts create mode 100644 scripts/extern-configs/types.ts create mode 100644 src/assets/extern-logos/hyper-liquid-grayscale.svg create mode 100644 src/assets/extern-logos/hyper-liquid.svg diff --git a/scripts/extern-configs/hyper-liquid/assets/grayscale-logo.svg b/scripts/extern-configs/hyper-liquid/assets/grayscale-logo.svg new file mode 100644 index 000000000..8ff58c997 --- /dev/null +++ b/scripts/extern-configs/hyper-liquid/assets/grayscale-logo.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/scripts/extern-configs/hyper-liquid/assets/logo.svg b/scripts/extern-configs/hyper-liquid/assets/logo.svg new file mode 100644 index 000000000..f93fce225 --- /dev/null +++ b/scripts/extern-configs/hyper-liquid/assets/logo.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/scripts/extern-configs/hyper-liquid/index.ts b/scripts/extern-configs/hyper-liquid/index.ts new file mode 100644 index 000000000..4475023ca --- /dev/null +++ b/scripts/extern-configs/hyper-liquid/index.ts @@ -0,0 +1,13 @@ +import { CHAIN_IDs } from "@across-protocol/constants"; +import { ExternalProjectConfig } from "../types"; + +export default { + name: "Hyper Liquid", + projectId: "hyper-liquid", + explorer: "https://arbiscan.io", + logoPath: "./assets/logo.svg", + grayscaleLogoPath: "./assets/grayscale-logo.svg", + publicRpcUrl: "https://arbitrum.publicnode.com", + intermediaryChain: CHAIN_IDs.ARBITRUM, + tokens: ["USDC"], +} as ExternalProjectConfig; diff --git a/scripts/extern-configs/index.ts b/scripts/extern-configs/index.ts new file mode 100644 index 000000000..e4a078e5d --- /dev/null +++ b/scripts/extern-configs/index.ts @@ -0,0 +1 @@ +export { default as HYPER_LIQUID } from "./hyper-liquid"; diff --git a/scripts/extern-configs/types.ts b/scripts/extern-configs/types.ts new file mode 100644 index 000000000..ceb2fa88e --- /dev/null +++ b/scripts/extern-configs/types.ts @@ -0,0 +1,13 @@ +// Destination only projects that are supported through a message bridge +// at a known supported intermediary chain +export type ExternalProjectConfig = { + projectId: string; + name: string; + fullName?: string; + explorer: string; + publicRpcUrl: string; + logoPath: string; + grayscaleLogoPath: string; + intermediaryChain: number; + tokens: string[]; +}; diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index 60d2980b4..29cb696a5 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -6,6 +6,8 @@ import { writeFileSync } from "fs"; import * as prettier from "prettier"; import path from "path"; import * as chainConfigs from "./chain-configs"; +import * as externConfigs from "./extern-configs"; +import assert from "assert"; function getTokenSymbolForLogo(tokenSymbol: string): string { switch (tokenSymbol) { @@ -29,6 +31,8 @@ type ToToken = ToChain["tokens"][number]; type SwapToken = ToChain["swapTokens"][number]; type ValidTokenSymbol = string; +const enabledMainnetExternalProjects = [externConfigs.HYPER_LIQUID]; + const enabledMainnetChainConfigs = [ chainConfigs.MAINNET, chainConfigs.OPTIMISM, @@ -107,7 +111,10 @@ const enabledRoutes = { // [CHAIN_IDs.BASE]: "0xbcfbCE9D92A516e3e7b0762AE218B4194adE34b4", }, }, - routes: transformChainConfigs(enabledMainnetChainConfigs), + routes: transformChainConfigs( + enabledMainnetChainConfigs, + enabledMainnetExternalProjects + ), }, [CHAIN_IDs.SEPOLIA]: { hubPoolChain: CHAIN_IDs.SEPOLIA, @@ -134,18 +141,21 @@ const enabledRoutes = { "0x17496824Ba574A4e9De80110A91207c4c63e552a", // Mocked }, }, - routes: transformChainConfigs(enabledSepoliaChainConfigs), + routes: transformChainConfigs(enabledSepoliaChainConfigs, []), }, } as const; function transformChainConfigs( - enabledChainConfigs: typeof enabledMainnetChainConfigs + enabledChainConfigs: typeof enabledMainnetChainConfigs, + enabledExternalProjects: typeof enabledMainnetExternalProjects ) { const transformedChainConfigs: { fromChain: number; fromSpokeAddress: string; + externalProjectId?: string; toChains: { chainId: number; + externalProjectId?: string; tokens: ( | string | { @@ -193,115 +203,7 @@ function transformChainConfigs( throw new Error(`No config found for chain ${toChainId}`); } - const tokens = chainConfig.tokens.flatMap((token) => { - const tokenSymbol = typeof token === "string" ? token : token.symbol; - - // Handle native USDC -> bridged USDC routes - if (tokenSymbol === "USDC") { - if (toChainConfig.enableCCTP) { - return [ - "USDC", - { - inputTokenSymbol: "USDC", - outputTokenSymbol: getBridgedUsdcSymbol(toChainConfig.chainId), - }, - ]; - } else if ( - toChainConfig.tokens.find( - (token) => - typeof token === "string" && sdkUtils.isBridgedUsdc(token) - ) - ) { - return [ - { - inputTokenSymbol: "USDC", - outputTokenSymbol: getBridgedUsdcSymbol(toChainConfig.chainId), - }, - ]; - } - } - - // Handle bridged USDC -> native/bridged USDC routes - if (sdkUtils.isBridgedUsdc(tokenSymbol)) { - if (toChainConfig.enableCCTP) { - return [ - { - inputTokenSymbol: tokenSymbol, - outputTokenSymbol: "USDC", - }, - { - inputTokenSymbol: tokenSymbol, - outputTokenSymbol: getBridgedUsdcSymbol(toChainConfig.chainId), - }, - ]; - } else if (toChainConfig.tokens.includes("USDC")) { - return [ - { - inputTokenSymbol: tokenSymbol, - outputTokenSymbol: "USDC", - }, - ]; - } else if ( - toChainConfig.tokens.find( - (token) => - typeof token === "string" && sdkUtils.isBridgedUsdc(token) - ) - ) { - return [ - { - inputTokenSymbol: tokenSymbol, - outputTokenSymbol: getBridgedUsdcSymbol(toChainConfig.chainId), - }, - ]; - } - } - - // Handle USDB -> DAI - if (tokenSymbol === "USDB" && toChainConfig.tokens.includes("DAI")) { - return [ - { - inputTokenSymbol: "USDB", - outputTokenSymbol: "DAI", - }, - ]; - } - if (tokenSymbol === "DAI" && toChainConfig.tokens.includes("USDB")) { - return [ - { - inputTokenSymbol: "DAI", - outputTokenSymbol: "USDB", - }, - ]; - } - - // Handle WETH Polygon & other non-eth chains - if ( - tokenSymbol === "WETH" && - !toChainConfig.tokens.includes("ETH") && - chainConfig.tokens.includes("ETH") - ) { - return ["WETH", "ETH"]; - } - - const chainIds = - typeof token === "string" ? [toChainId] : token.chainIds; - - const toToken = toChainConfig.tokens.find((token) => - typeof token === "string" - ? token === tokenSymbol - : token.symbol === tokenSymbol - ); - if ( - !toToken || - (typeof toToken === "object" && - !toToken.chainIds.includes(fromChainId)) || - !chainIds.includes(toChainId) - ) { - return []; - } - - return tokenSymbol; - }); + const tokens = processTokenRoutes(chainConfig, toChainConfig); // Handle USDC swap tokens const usdcSwapTokens = chainConfig.enableCCTP @@ -328,6 +230,44 @@ function transformChainConfigs( toChains.push(toChain); } + for (const externalProject of enabledExternalProjects) { + const associatedChain = enabledChainConfigs.find( + (config) => config.chainId === externalProject.intermediaryChain + ); + assert(associatedChain, "Associated chain not found"); + + let associatedRoutes = processTokenRoutes( + chainConfig, + { ...associatedChain, enableCCTP: false }, + externalProject.tokens + ); + + const externalProjectId = externalProject.projectId; + + // Handle USDC swap tokens + const usdcSwapTokens = []; + + const toChain = { + chainId: externalProject.intermediaryChain, + externalProjectId, + tokens: associatedRoutes, + swapTokens: usdcSwapTokens.filter( + ({ acrossInputTokenSymbol, acrossOutputTokenSymbol }) => + associatedRoutes.some((token) => + typeof token === "string" + ? token === acrossInputTokenSymbol + : token.inputTokenSymbol === acrossInputTokenSymbol + ) && + associatedRoutes.some((token) => + typeof token === "string" + ? token === acrossOutputTokenSymbol + : token.outputTokenSymbol === acrossOutputTokenSymbol + ) + ), + }; + toChains.push(toChain); + } + transformedChainConfigs.push({ fromChain: fromChainId, fromSpokeAddress, @@ -338,6 +278,126 @@ function transformChainConfigs( return transformedChainConfigs; } +function processTokenRoutes( + fromConfig: typeof chainConfigs.MAINNET, + toConfig: typeof chainConfigs.MAINNET, + tokensToProcess?: string[] +) { + const toChainId = toConfig.chainId; + const tokens = tokensToProcess ?? fromConfig.tokens; + return tokens.flatMap((token) => { + const tokenSymbol = typeof token === "string" ? token : token.symbol; + + // If the fromConfig does not support the token, return an empty array + if (!fromConfig.tokens.includes(tokenSymbol)) { + return []; + } + + // Handle native USDC -> bridged USDC routes + if (tokenSymbol === "USDC") { + if (toConfig.enableCCTP) { + return [ + "USDC", + { + inputTokenSymbol: "USDC", + outputTokenSymbol: getBridgedUsdcSymbol(toChainId), + }, + ]; + } else if ( + toConfig.tokens.find( + (token) => typeof token === "string" && sdkUtils.isBridgedUsdc(token) + ) + ) { + return [ + { + inputTokenSymbol: "USDC", + outputTokenSymbol: getBridgedUsdcSymbol(toChainId), + }, + ]; + } + } + + // Handle bridged USDC -> native/bridged USDC routes + if (sdkUtils.isBridgedUsdc(tokenSymbol)) { + if (toConfig.enableCCTP) { + return [ + { + inputTokenSymbol: tokenSymbol, + outputTokenSymbol: "USDC", + }, + { + inputTokenSymbol: tokenSymbol, + outputTokenSymbol: getBridgedUsdcSymbol(toChainId), + }, + ]; + } else if (toConfig.tokens.includes("USDC")) { + return [ + { + inputTokenSymbol: tokenSymbol, + outputTokenSymbol: "USDC", + }, + ]; + } else if ( + toConfig.tokens.find( + (token) => typeof token === "string" && sdkUtils.isBridgedUsdc(token) + ) + ) { + return [ + { + inputTokenSymbol: tokenSymbol, + outputTokenSymbol: getBridgedUsdcSymbol(toChainId), + }, + ]; + } + } + + // Handle USDB -> DAI + if (tokenSymbol === "USDB" && toConfig.tokens.includes("DAI")) { + return [ + { + inputTokenSymbol: "USDB", + outputTokenSymbol: "DAI", + }, + ]; + } + if (tokenSymbol === "DAI" && toConfig.tokens.includes("USDB")) { + return [ + { + inputTokenSymbol: "DAI", + outputTokenSymbol: "USDB", + }, + ]; + } + + // Handle WETH Polygon & other non-eth chains + if ( + tokenSymbol === "WETH" && + !toConfig.tokens.includes("ETH") && + fromConfig.tokens.includes("ETH") + ) { + return ["WETH", "ETH"]; + } + + const chainIds = typeof token === "string" ? [toChainId] : token.chainIds; + + const toToken = toConfig.tokens.find((token) => + typeof token === "string" + ? token === tokenSymbol + : token.symbol === tokenSymbol + ); + if ( + !toToken || + (typeof toToken === "object" && + !toToken.chainIds.includes(fromConfig.chainId)) || + !chainIds.includes(toChainId) + ) { + return []; + } + + return tokenSymbol; + }); +} + async function generateRoutes(hubPoolChainId = 1) { const config = enabledRoutes[hubPoolChainId]; @@ -559,6 +619,7 @@ function transformToRoute( toTokenSymbol: outputTokenSymbol, isNative: inputTokenSymbol === TOKEN_SYMBOLS_MAP.ETH.symbol, l1TokenAddress: inputToken.l1TokenAddress, + externalProjectId: toChain.externalProjectId, }; } diff --git a/scripts/generate-ui-assets.ts b/scripts/generate-ui-assets.ts index 6a44ccfb3..388015360 100644 --- a/scripts/generate-ui-assets.ts +++ b/scripts/generate-ui-assets.ts @@ -3,10 +3,12 @@ import * as prettier from "prettier"; import { camelCase, capitalize } from "lodash-es"; import * as chainConfigs from "./chain-configs"; +import * as externConfigs from "./extern-configs"; const chainsConstantsFileTargetDir = process.cwd() + "/src/constants/chains"; const chainsConstantsFilePath = chainsConstantsFileTargetDir + "/configs.ts"; const chainAssetsTargetDir = process.cwd() + "/src/assets/chain-logos"; +const externAssetsTargetDir = process.cwd() + "/src/assets/extern-logos"; async function generateUiAssets() { const chainsFileImports: string[] = [ @@ -14,7 +16,7 @@ async function generateUiAssets() { ]; const chainsFileContent: string[] = []; const chainVarNames: string[] = []; - + const externVarNames: string[] = []; for (const [chainKey, chainConfig] of Object.entries(chainConfigs)) { const { chainId, logoPath, grayscaleLogoPath, name, fullName } = chainConfig; @@ -66,6 +68,62 @@ async function generateUiAssets() { `); chainVarNames.push(chainVarName); } + + // Process external project configs + for (const [projectKey, projectConfig] of Object.entries(externConfigs)) { + if (projectKey === "types") continue; // Skip the types file + + const { projectId, logoPath, grayscaleLogoPath, name, fullName } = + projectConfig; + + // Copy logos into assets directory + const assetFileNameBase = projectKey.toLowerCase().replace("_", "-"); + const projectLogoTargetFileName = assetFileNameBase + ".svg"; + const projectGrayscaleLogoTargetFileName = + assetFileNameBase + "-grayscale.svg"; + const projectConfigSrcDir = + process.cwd() + "/scripts/extern-configs/" + assetFileNameBase + "/"; + + copyFileSync( + projectConfigSrcDir + logoPath, + externAssetsTargetDir + "/" + projectLogoTargetFileName + ); + copyFileSync( + projectConfigSrcDir + grayscaleLogoPath, + externAssetsTargetDir + "/" + projectGrayscaleLogoTargetFileName + ); + + // Generate external project constants + const projectVarName = camelCase(name); + const projectLogoVarName = camelCase(name + "Logo"); + const projectGrayscaleLogoVarName = camelCase(name + "GrayscaleLogo"); + const projectLogoSvgVarName = projectLogoVarName + "Svg"; + const projectGrayscaleLogoSvgVarName = projectGrayscaleLogoVarName + "Svg"; + + chainsFileImports.push(` + import ${projectLogoVarName} from "assets/extern-logos/${projectLogoTargetFileName}"; + import ${projectGrayscaleLogoVarName} from "assets/extern-logos/${projectGrayscaleLogoTargetFileName}"; + import {ReactComponent as ${projectLogoSvgVarName}} from "assets/extern-logos/${projectLogoTargetFileName}"; + import {ReactComponent as ${projectGrayscaleLogoSvgVarName}} from "assets/extern-logos/${projectGrayscaleLogoTargetFileName}"; + `); + + chainsFileContent.push(` + export const ${projectVarName} = { + name: "${name}", + fullName: "${fullName || capitalize(name)}", + projectId: "${projectId}", + logoURI: ${projectLogoVarName}, + grayscaleLogoURI: ${projectGrayscaleLogoVarName}, + logoSvg: ${projectLogoSvgVarName}, + grayscaleLogoSvg: ${projectGrayscaleLogoSvgVarName}, + explorerUrl: "${projectConfig.explorer}", + rpcUrl: "${projectConfig.publicRpcUrl}", + intermediaryChain: ${projectConfig.intermediaryChain}, + }; + `); + externVarNames.push(projectVarName); + } + chainsFileContent.push(` export const chainConfigs = [${chainVarNames.join(", ")}].reduce((acc, chain) => { acc[chain.chainId] = chain; @@ -73,6 +131,13 @@ async function generateUiAssets() { }, {} as Record); `); + chainsFileContent.push(` + export const externConfigs = [${externVarNames.join(", ")}].reduce((acc, extern) => { + acc[extern.projectId] = extern; + return acc; + }, {} as Record); + `); + // Write chains file const chainsFileContentStr = chainsFileImports.join("\n") + chainsFileContent.join("\n"); diff --git a/src/assets/extern-logos/hyper-liquid-grayscale.svg b/src/assets/extern-logos/hyper-liquid-grayscale.svg new file mode 100644 index 000000000..8ff58c997 --- /dev/null +++ b/src/assets/extern-logos/hyper-liquid-grayscale.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/assets/extern-logos/hyper-liquid.svg b/src/assets/extern-logos/hyper-liquid.svg new file mode 100644 index 000000000..f93fce225 --- /dev/null +++ b/src/assets/extern-logos/hyper-liquid.svg @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/src/constants/chains/configs.ts b/src/constants/chains/configs.ts index 72ac85862..981387ef4 100644 --- a/src/constants/chains/configs.ts +++ b/src/constants/chains/configs.ts @@ -120,6 +120,11 @@ import zoraGrayscaleLogo from "assets/chain-logos/zora-grayscale.svg"; import { ReactComponent as zoraLogoSvg } from "assets/chain-logos/zora.svg"; import { ReactComponent as zoraGrayscaleLogoSvg } from "assets/chain-logos/zora-grayscale.svg"; +import hyperLiquidLogo from "assets/extern-logos/hyper-liquid.svg"; +import hyperLiquidGrayscaleLogo from "assets/extern-logos/hyper-liquid-grayscale.svg"; +import { ReactComponent as hyperLiquidLogoSvg } from "assets/extern-logos/hyper-liquid.svg"; +import { ReactComponent as hyperLiquidGrayscaleLogoSvg } from "assets/extern-logos/hyper-liquid-grayscale.svg"; + export const alephZero = { name: "Aleph Zero", fullName: "Aleph Zero", @@ -523,6 +528,19 @@ export const zora = { pollingInterval: 2000, }; +export const hyperLiquid = { + name: "Hyper Liquid", + fullName: "Hyper liquid", + projectId: "hyper-liquid", + logoURI: hyperLiquidLogo, + grayscaleLogoURI: hyperLiquidGrayscaleLogo, + logoSvg: hyperLiquidLogoSvg, + grayscaleLogoSvg: hyperLiquidGrayscaleLogoSvg, + explorerUrl: "https://arbiscan.io", + rpcUrl: "https://arbitrum.publicnode.com", + intermediaryChain: 42161, +}; + export const chainConfigs = [ alephZero, arbitrum, @@ -555,3 +573,11 @@ export const chainConfigs = [ }, {} as Record ); + +export const externConfigs = [hyperLiquid].reduce( + (acc, extern) => { + acc[extern.projectId] = extern; + return acc; + }, + {} as Record +); diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index 7b5cff05c..b76c24e9c 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -989,6 +989,18 @@ "isNative": true, "l1TokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, + { + "fromChain": 1, + "toChain": 42161, + "fromTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "toTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "fromSpokeAddress": "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5", + "fromTokenSymbol": "USDC", + "toTokenSymbol": "USDC", + "isNative": false, + "l1TokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "externalProjectId": "hyper-liquid" + }, { "fromChain": 10, "toChain": 1, @@ -1935,6 +1947,18 @@ "isNative": true, "l1TokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, + { + "fromChain": 10, + "toChain": 42161, + "fromTokenAddress": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", + "toTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "fromSpokeAddress": "0x6f26Bf09B1C792e3228e5467807a900A503c0281", + "fromTokenSymbol": "USDC", + "toTokenSymbol": "USDC", + "isNative": false, + "l1TokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "externalProjectId": "hyper-liquid" + }, { "fromChain": 137, "toChain": 1, @@ -2705,6 +2729,18 @@ "isNative": false, "l1TokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, + { + "fromChain": 137, + "toChain": 42161, + "fromTokenAddress": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "toTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "fromSpokeAddress": "0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096", + "fromTokenSymbol": "USDC", + "toTokenSymbol": "USDC", + "isNative": false, + "l1TokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "externalProjectId": "hyper-liquid" + }, { "fromChain": 42161, "toChain": 1, @@ -3640,6 +3676,18 @@ "isNative": true, "l1TokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, + { + "fromChain": 42161, + "toChain": 42161, + "fromTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "toTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "fromSpokeAddress": "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A", + "fromTokenSymbol": "USDC", + "toTokenSymbol": "USDC", + "isNative": false, + "l1TokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "externalProjectId": "hyper-liquid" + }, { "fromChain": 324, "toChain": 1, @@ -5070,6 +5118,18 @@ "isNative": true, "l1TokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, + { + "fromChain": 8453, + "toChain": 42161, + "fromTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "toTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "fromSpokeAddress": "0x09aea4b2242abC8bb4BB78D537A67a245A7bEC64", + "fromTokenSymbol": "USDC", + "toTokenSymbol": "USDC", + "isNative": false, + "l1TokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "externalProjectId": "hyper-liquid" + }, { "fromChain": 59144, "toChain": 1, @@ -8359,6 +8419,18 @@ "isNative": true, "l1TokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, + { + "fromChain": 534352, + "toChain": 42161, + "fromTokenAddress": "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4", + "toTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + "fromSpokeAddress": "0x3baD7AD0728f9917d1Bf08af5782dCbD516cDd96", + "fromTokenSymbol": "USDC", + "toTokenSymbol": "USDC", + "isNative": false, + "l1TokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "externalProjectId": "hyper-liquid" + }, { "fromChain": 690, "toChain": 1, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 46b82c189..49508b541 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -294,6 +294,7 @@ const RouteSS = superstruct.object({ toTokenSymbol: superstruct.string(), isNative: superstruct.boolean(), l1TokenAddress: superstruct.string(), + externalProjectId: superstruct.optional(superstruct.string()), }); const RoutesSS = superstruct.array(RouteSS); const SwapRouteSS = superstruct.assign( diff --git a/src/views/Bridge/components/ChainSelector.tsx b/src/views/Bridge/components/ChainSelector.tsx index a81a676a7..b737f1d17 100644 --- a/src/views/Bridge/components/ChainSelector.tsx +++ b/src/views/Bridge/components/ChainSelector.tsx @@ -17,6 +17,7 @@ import { getAllChains } from "../utils"; import { useBalanceBySymbolPerChain, useConnection } from "hooks"; import { useMemo } from "react"; import { BigNumber } from "ethers"; +import { externConfigs } from "constants/chains/configs"; type Props = { selectedRoute: Route; @@ -34,7 +35,13 @@ export function ChainSelector({ onSelectChain, }: Props) { const isFrom = fromOrTo === "from"; - const { fromChain, toChain, fromTokenSymbol, toTokenSymbol } = selectedRoute; + const { + fromChain, + toChain, + fromTokenSymbol, + toTokenSymbol, + externalProjectId, + } = selectedRoute; const selectedChain = getChainInfo(isFrom ? fromChain : toChain); const tokenInfo = getToken(isFrom ? fromTokenSymbol : toTokenSymbol); @@ -80,7 +87,14 @@ export function ChainSelector({ elements={sortOrder.map((chain) => ({ value: chain.chainId, - element: , + element: ( + + ), suffix: isConnected && isFrom ? ( @@ -121,14 +135,19 @@ export function ChainSelector({ function ChainInfoElement({ chain, + externalProjectId, superText, }: { chain: ChainInfo; + externalProjectId?: string; superText?: string; }) { + const externalProject = externalProjectId + ? externConfigs[externalProjectId] + : null; return ( - + {superText && ( @@ -136,7 +155,9 @@ function ChainInfoElement({ )} - {capitalizeFirstLetter(chain.fullName ?? chain.name)} + {capitalizeFirstLetter( + externalProject?.name ?? chain.fullName ?? chain.name + )} From bff88102ec0d06926db7fa91ea2fded0eb1fafde Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 9 Jan 2025 11:23:04 -0500 Subject: [PATCH 2/9] chore: select hyperliquid Signed-off-by: james-a-morris --- scripts/generate-routes.ts | 3 + src/components/Selector/Selector.tsx | 6 +- src/components/Selector/useSelector.ts | 5 +- ...6fA914353c44b2E33eBE05f21846F1048bEda.json | 12 -- src/views/Bridge/components/BridgeForm.tsx | 4 +- src/views/Bridge/components/ChainSelector.tsx | 54 +++++---- src/views/Bridge/components/TokenSelector.tsx | 26 ++++- src/views/Bridge/hooks/useSelectRoute.ts | 105 ++++++++++++++---- src/views/Bridge/utils.ts | 66 +++++++++-- 9 files changed, 198 insertions(+), 83 deletions(-) diff --git a/scripts/generate-routes.ts b/scripts/generate-routes.ts index 29cb696a5..8ec0f2469 100644 --- a/scripts/generate-routes.ts +++ b/scripts/generate-routes.ts @@ -231,6 +231,9 @@ function transformChainConfigs( } for (const externalProject of enabledExternalProjects) { + if (externalProject.intermediaryChain === fromChainId) { + continue; + } const associatedChain = enabledChainConfigs.find( (config) => config.chainId === externalProject.intermediaryChain ); diff --git a/src/components/Selector/Selector.tsx b/src/components/Selector/Selector.tsx index fa8a9c25c..55d617d0a 100644 --- a/src/components/Selector/Selector.tsx +++ b/src/components/Selector/Selector.tsx @@ -85,7 +85,11 @@ const Selector = ({ 6}> {elements.map((element, idx) => ( { if (element.disabled && !allowSelectDisabled) { return; diff --git a/src/components/Selector/useSelector.ts b/src/components/Selector/useSelector.ts index 7099c3fe8..6f5596245 100644 --- a/src/components/Selector/useSelector.ts +++ b/src/components/Selector/useSelector.ts @@ -1,14 +1,15 @@ import useCurrentBreakpoint from "hooks/useCurrentBreakpoint"; import { useState } from "react"; import { SelectorElementType } from "./Selector"; +import { isEqual } from "lodash-es"; export function useSelector( elements: SelectorElementType[], selectedValue: ValueType ) { const [displayModal, setDisplayModal] = useState(false); - const selectedIndex = elements.findIndex( - (element) => element.value === selectedValue + const selectedIndex = elements.findIndex((element) => + isEqual(element.value, selectedValue) ); const { isMobile } = useCurrentBreakpoint(); diff --git a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json index b76c24e9c..826b804e4 100644 --- a/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json +++ b/src/data/routes_1_0xc186fA914353c44b2E33eBE05f21846F1048bEda.json @@ -3676,18 +3676,6 @@ "isNative": true, "l1TokenAddress": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" }, - { - "fromChain": 42161, - "toChain": 42161, - "fromTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", - "toTokenAddress": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", - "fromSpokeAddress": "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A", - "fromTokenSymbol": "USDC", - "toTokenSymbol": "USDC", - "isNative": false, - "l1TokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "externalProjectId": "hyper-liquid" - }, { "fromChain": 324, "toChain": 1, diff --git a/src/views/Bridge/components/BridgeForm.tsx b/src/views/Bridge/components/BridgeForm.tsx index f944570e6..45c518514 100644 --- a/src/views/Bridge/components/BridgeForm.tsx +++ b/src/views/Bridge/components/BridgeForm.tsx @@ -47,8 +47,8 @@ export type BridgeFormProps = { onClickMaxBalance: VoidHandler; onSelectInputToken: (token: string) => void; onSelectOutputToken: (token: string) => void; - onSelectFromChain: (chainId: number) => void; - onSelectToChain: (chainId: number) => void; + onSelectFromChain: (chainId: number, externalProjectId?: string) => void; + onSelectToChain: (chainId: number, externalProjectId?: string) => void; onClickQuickSwap: VoidHandler; onClickChainSwitch: VoidHandler; onClickActionButton: VoidHandler; diff --git a/src/views/Bridge/components/ChainSelector.tsx b/src/views/Bridge/components/ChainSelector.tsx index b737f1d17..d37c951fe 100644 --- a/src/views/Bridge/components/ChainSelector.tsx +++ b/src/views/Bridge/components/ChainSelector.tsx @@ -17,13 +17,12 @@ import { getAllChains } from "../utils"; import { useBalanceBySymbolPerChain, useConnection } from "hooks"; import { useMemo } from "react"; import { BigNumber } from "ethers"; -import { externConfigs } from "constants/chains/configs"; type Props = { selectedRoute: Route; fromOrTo: "from" | "to"; toAddress?: string; - onSelectChain: (chainId: number) => void; + onSelectChain: (chainId: number, externalProjectId?: string) => void; }; const allChains = getAllChains(); @@ -35,13 +34,8 @@ export function ChainSelector({ onSelectChain, }: Props) { const isFrom = fromOrTo === "from"; - const { - fromChain, - toChain, - fromTokenSymbol, - toTokenSymbol, - externalProjectId, - } = selectedRoute; + const { fromChain, toChain, fromTokenSymbol, toTokenSymbol } = selectedRoute; + const selectedChain = getChainInfo(isFrom ? fromChain : toChain); const tokenInfo = getToken(isFrom ? fromTokenSymbol : toTokenSymbol); @@ -63,6 +57,7 @@ export function ChainSelector({ return chains; } else { return chains + .filter((c) => !c.projectId) .map((c) => ({ ...c, disabled: c.balance.eq(0), @@ -84,17 +79,13 @@ export function ChainSelector({ }, [balances, isConnected, isFrom]); return ( - + elements={sortOrder.map((chain) => ({ - value: chain.chainId, - element: ( - - ), + value: { + chainId: chain.chainId, + externalProjectId: chain.projectId, + }, + element: , suffix: isConnected && isFrom ? ( @@ -114,8 +105,13 @@ export function ChainSelector({ /> ) : undefined } - selectedValue={isFrom ? fromChain : toChain} - setSelectedValue={onSelectChain} + selectedValue={{ + chainId: isFrom ? fromChain : toChain, + externalProjectId: isFrom ? undefined : selectedRoute.externalProjectId, + }} + setSelectedValue={(val) => + onSelectChain(val.chainId, val.externalProjectId) + } title={ @@ -138,16 +134,18 @@ function ChainInfoElement({ externalProjectId, superText, }: { - chain: ChainInfo; + chain: Pick; externalProjectId?: string; superText?: string; }) { - const externalProject = externalProjectId - ? externConfigs[externalProjectId] - : null; + // const externalProject = externalProjectId + // ? externConfigs[externalProjectId] + // : null; + externalProjectId; + return ( - + {superText && ( @@ -155,9 +153,7 @@ function ChainInfoElement({ )} - {capitalizeFirstLetter( - externalProject?.name ?? chain.fullName ?? chain.name - )} + {capitalizeFirstLetter(chain.fullName ?? chain.name)} diff --git a/src/views/Bridge/components/TokenSelector.tsx b/src/views/Bridge/components/TokenSelector.tsx index 5052912a8..7f0f3f622 100644 --- a/src/views/Bridge/components/TokenSelector.tsx +++ b/src/views/Bridge/components/TokenSelector.tsx @@ -39,7 +39,14 @@ export function TokenSelector({ receiveTokenSymbol, }: Props) { const isInputTokenSelector = inputOrOutputToken === "input"; - const { fromChain, toChain, fromTokenSymbol, toTokenSymbol } = selectedRoute; + const { + fromChain, + toChain, + fromTokenSymbol, + toTokenSymbol, + externalProjectId, + } = selectedRoute; + const selectedToken = getToken( isInputTokenSelector ? selectedRoute.type === "swap" @@ -59,8 +66,13 @@ export function TokenSelector({ } > = useMemo(() => { const availableTokens = isInputTokenSelector - ? getAvailableInputTokens(fromChain, toChain) - : getAvailableOutputTokens(fromChain, toChain, fromTokenSymbol); + ? getAvailableInputTokens(fromChain, toChain, externalProjectId) + : getAvailableOutputTokens( + fromChain, + toChain, + fromTokenSymbol, + externalProjectId + ); const orderedAvailableTokens = tokenList.filter((orderedToken) => availableTokens.find( (availableToken) => availableToken.symbol === orderedToken.symbol @@ -79,7 +91,13 @@ export function TokenSelector({ .map((t) => ({ ...t, disabled: true })) : []), ]; - }, [fromChain, toChain, fromTokenSymbol, isInputTokenSelector]); + }, [ + fromChain, + toChain, + fromTokenSymbol, + isInputTokenSelector, + externalProjectId, + ]); const { balances } = useBalancesBySymbols({ tokenSymbols: orderedTokens.filter((t) => !t.disabled).map((t) => t.symbol), diff --git a/src/views/Bridge/hooks/useSelectRoute.ts b/src/views/Bridge/hooks/useSelectRoute.ts index 29693b944..f5fe31568 100644 --- a/src/views/Bridge/hooks/useSelectRoute.ts +++ b/src/views/Bridge/hooks/useSelectRoute.ts @@ -118,7 +118,7 @@ export function useSelectRoute() { ); const handleSelectFromChain = useCallback( - (fromChainId: number) => { + (fromChainId: number, _externalProjectId?: string) => { const isSwap = selectedRoute.type === "swap"; const filterBy = { inputTokenSymbol: isSwap ? undefined : selectedRoute.fromTokenSymbol, @@ -129,11 +129,14 @@ export function useSelectRoute() { ), fromChain: fromChainId, toChain: selectedRoute.toChain, + externalProjectId: selectedRoute.externalProjectId, }; + const similarTokenSymbols = similarTokensMap[ isSwap ? selectedRoute.swapTokenSymbol : selectedRoute.fromTokenSymbol ] || []; + const findNextBestRouteBySimilarToken = ( priorityFilterKeys: PriorityFilterKey[] ) => { @@ -147,7 +150,24 @@ export function useSelectRoute() { } } }; + const route = + // First try with external project ID if it exists + (filterBy.externalProjectId && + (findNextBestRoute( + [ + "fromChain", + "toChain", + "externalProjectId", + isSwap ? "swapTokenSymbol" : "inputTokenSymbol", + ], + filterBy + ) || + findNextBestRoute( + ["fromChain", "toChain", "externalProjectId"], + filterBy + ))) || + // Then try without external project ID constraints findNextBestRoute( [ "fromChain", @@ -165,6 +185,7 @@ export function useSelectRoute() { findNextBestRoute(["fromChain", "toChain"], { ...filterBy, outputTokenSymbol: undefined, + externalProjectId: undefined, }) || findNextBestRoute(["fromChain"], { fromChain: fromChainId, @@ -185,7 +206,7 @@ export function useSelectRoute() { ); const handleSelectToChain = useCallback( - (toChainId: number) => { + (toChainId: number, externalProjectId?: string) => { const isSwap = selectedRoute.type === "swap"; const filterBy = { inputTokenSymbol: isSwap ? undefined : selectedRoute.fromTokenSymbol, @@ -196,29 +217,65 @@ export function useSelectRoute() { ), fromChain: selectedRoute.fromChain, toChain: toChainId, + externalProjectId, }; - const route = - findNextBestRoute( - [ - "fromChain", - "toChain", - isSwap ? "swapTokenSymbol" : "inputTokenSymbol", - ], - filterBy - ) || - findNextBestRoute(["fromChain", "toChain"], filterBy) || - findNextBestRoute(["fromChain", "toChain"], { - ...filterBy, - outputTokenSymbol: undefined, - }) || - findNextBestRoute(["fromChain"], { - toChain: toChainId, - }) || - findNextBestRoute( - ["toChain", isSwap ? "swapTokenSymbol" : "inputTokenSymbol"], - filterBy - ) || - initialRoute; + + // Try to find route with exact match first + let route = externalProjectId + ? findNextBestRoute(["toChain", "externalProjectId"], { + toChain: toChainId, + externalProjectId, + }) + : findNextBestRoute(["toChain"], { + toChain: toChainId, + externalProjectId: undefined, + }); + + // If no route found, fall back to previous logic + if (!route) { + route = + findNextBestRoute( + [ + "fromChain", + "toChain", + "externalProjectId", + isSwap ? "swapTokenSymbol" : "inputTokenSymbol", + ], + filterBy + ) || + findNextBestRoute( + ["fromChain", "toChain", "externalProjectId"], + filterBy + ) || + (externalProjectId && + findNextBestRoute(["fromChain", "toChain", "externalProjectId"], { + fromChain: selectedRoute.fromChain, + toChain: toChainId, + externalProjectId, + })) || + (externalProjectId === undefined && + (findNextBestRoute( + [ + "fromChain", + "toChain", + isSwap ? "swapTokenSymbol" : "inputTokenSymbol", + ], + { ...filterBy, externalProjectId: undefined } + ) || + findNextBestRoute(["fromChain", "toChain"], { + ...filterBy, + outputTokenSymbol: undefined, + externalProjectId: undefined, + }))) || + findNextBestRoute(["fromChain"], { + toChain: toChainId, + }) || + findNextBestRoute( + ["toChain", isSwap ? "swapTokenSymbol" : "inputTokenSymbol"], + filterBy + ) || + initialRoute; + } setSelectedRoute(route); diff --git a/src/views/Bridge/utils.ts b/src/views/Bridge/utils.ts index c1c023768..12cb18e15 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -1,3 +1,4 @@ +import { externConfigs } from "constants/chains/configs"; import { BigNumber } from "ethers"; import { Route, @@ -29,6 +30,7 @@ type RouteFilter = Partial<{ outputTokenSymbol: string; fromChain: number; toChain: number; + externalProjectId: string; }>; export enum AmountInputError { @@ -170,6 +172,7 @@ export function findEnabledRoute( swapTokenSymbol, fromChain, toChain, + externalProjectId, } = filter; const commonRouteFilter = (route: Route | SwapRoute) => @@ -180,7 +183,10 @@ export function findEnabledRoute( ? route.toTokenSymbol.toUpperCase() === outputTokenSymbol.toUpperCase() : true) && (fromChain ? route.fromChain === fromChain : true) && - (toChain ? route.toChain === toChain : true); + (toChain ? route.toChain === toChain : true) && + (externalProjectId !== undefined + ? route.externalProjectId === externalProjectId + : true); if (swapTokenSymbol) { const swapRoute = swapRoutes.find( @@ -216,7 +222,8 @@ export type PriorityFilterKey = | "swapTokenSymbol" | "outputTokenSymbol" | "fromChain" - | "toChain"; + | "toChain" + | "externalProjectId"; /** * Returns the next best matching route based on the given priority keys and filter. * @param priorityFilterKeys Set of filter keys to use if no route is found based on `filter`. @@ -270,6 +277,7 @@ export function findNextBestRoute( "outputTokenSymbol", "fromChain", "toChain", + "externalProjectId", ] as const; const nonPriorityFilterKeys = allFilterKeys.filter((key) => priorityFilterKeys.includes(key) @@ -310,20 +318,23 @@ export function getAllTokens() { export function getAvailableInputTokens( selectedFromChain: number, - selectedToChain: number + selectedToChain: number, + externalProjectId?: string ) { const routeTokens = enabledRoutes .filter( (route) => route.fromChain === selectedFromChain && - route.toChain === selectedToChain + route.toChain === selectedToChain && + route.externalProjectId === externalProjectId ) .map((route) => getToken(route.fromTokenSymbol)); const swapTokens = swapRoutes .filter( (route) => route.fromChain === selectedFromChain && - route.toChain === selectedToChain + route.toChain === selectedToChain && + route.externalProjectId === externalProjectId ) .map((route) => getToken(route.swapTokenSymbol)); return [...routeTokens, ...swapTokens].filter( @@ -335,14 +346,16 @@ export function getAvailableInputTokens( export function getAvailableOutputTokens( selectedFromChain: number, selectedToChain: number, - selectedInputTokenSymbol: string + selectedInputTokenSymbol: string, + externalProjectId?: string ) { return enabledRoutes .filter( (route) => route.fromChain === selectedFromChain && route.toChain === selectedToChain && - route.fromTokenSymbol === selectedInputTokenSymbol + route.fromTokenSymbol === selectedInputTokenSymbol && + route.externalProjectId === externalProjectId ) .map((route) => getToken(route.toTokenSymbol)) .filter( @@ -353,11 +366,21 @@ export function getAvailableOutputTokens( export function getAllChains() { return enabledRoutes - .map((route) => getChainInfo(route.fromChain)) + .map((route) => + route.externalProjectId + ? { + ...externConfigs[route.externalProjectId], + chainId: externConfigs[route.externalProjectId].intermediaryChain, + } + : { ...getChainInfo(route.fromChain), projectId: undefined } + ) .filter( (chain, index, self) => index === - self.findIndex((fromChain) => fromChain.chainId === chain.chainId) + self.findIndex( + (fromChain) => + fromChain.chainId === chain.chainId && fromChain.name === chain.name + ) ) .sort((a, b) => { if (a.name < b.name) { @@ -370,6 +393,31 @@ export function getAllChains() { }); } +export function getOriginChains() { + return enabledRoutes + .filter( + (route, index, self) => + index === + self.findIndex( + (r) => + r.fromChain === route.fromChain && r.externalProjectId === undefined // HL is destination-only + ) + ) + .map((route) => + route.externalProjectId + ? { + ...externConfigs[route.externalProjectId], + chainId: externConfigs[route.externalProjectId].intermediaryChain, + } + : { ...getChainInfo(route.fromChain), projectId: undefined } + ) + .sort((a, b) => { + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); +} + export function getRouteFromUrl(overrides?: RouteFilter) { const params = new URLSearchParams(window.location.search); From c11100b474d055e6b5f88c7447e53e4e2f600755 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 9 Jan 2025 14:50:00 -0500 Subject: [PATCH 3/9] feat: bridge integration Signed-off-by: james-a-morris --- src/utils/sdk.ts | 1 + src/utils/url.ts | 3 + src/views/Bridge/Bridge.tsx | 1 - src/views/Bridge/components/BridgeForm.tsx | 2 +- src/views/Bridge/hooks/useBridge.ts | 1 + src/views/Bridge/hooks/useBridgeAction.ts | 156 +++++++++++++++++- src/views/Bridge/utils.ts | 12 +- src/views/DepositStatus/DepositStatus.tsx | 4 +- .../components/DepositStatusAnimatedIcons.tsx | 7 +- .../components/DepositStatusLowerCard.tsx | 3 + .../components/DepositStatusUpperCard.tsx | 5 +- 11 files changed, 188 insertions(+), 7 deletions(-) diff --git a/src/utils/sdk.ts b/src/utils/sdk.ts index b8b26a1bd..b78c2aeec 100644 --- a/src/utils/sdk.ts +++ b/src/utils/sdk.ts @@ -13,6 +13,7 @@ export { getCurrentTime } from "@across-protocol/sdk/dist/esm/utils/TimeUtils"; export { isBridgedUsdc } from "@across-protocol/sdk/dist/esm/utils/TokenUtils"; export { BRIDGED_USDC_SYMBOLS } from "@across-protocol/sdk/dist/esm/constants"; export { getNativeTokenSymbol } from "@across-protocol/sdk/dist/esm/utils/NetworkUtils"; +export { compareAddressesSimple } from "@across-protocol/sdk/dist/esm/utils/AddressUtils"; export function getUpdateV3DepositTypedData( depositId: number, diff --git a/src/utils/url.ts b/src/utils/url.ts index 12fe07011..8f5653d89 100644 --- a/src/utils/url.ts +++ b/src/utils/url.ts @@ -3,17 +3,20 @@ export function getBridgeUrlWithQueryParams({ toChainId, inputTokenSymbol, outputTokenSymbol, + externalProjectId, }: { fromChainId: number; toChainId: number; inputTokenSymbol: string; outputTokenSymbol?: string; + externalProjectId?: string; }) { const cleanParams = Object.entries({ from: fromChainId.toString(), to: toChainId.toString(), inputToken: inputTokenSymbol, outputToken: outputTokenSymbol, + externalProjectId, }).reduce((acc, [key, value]) => { if (value) { return { ...acc, [key]: value }; diff --git a/src/views/Bridge/Bridge.tsx b/src/views/Bridge/Bridge.tsx index c6f3baa23..ebb325945 100644 --- a/src/views/Bridge/Bridge.tsx +++ b/src/views/Bridge/Bridge.tsx @@ -40,7 +40,6 @@ const Bridge = () => { handleSetNewSlippage, isQuoteLoading, } = useBridge(); - return ( <> {toAccount && ( diff --git a/src/views/Bridge/components/BridgeForm.tsx b/src/views/Bridge/components/BridgeForm.tsx index 45c518514..20064eb61 100644 --- a/src/views/Bridge/components/BridgeForm.tsx +++ b/src/views/Bridge/components/BridgeForm.tsx @@ -219,7 +219,7 @@ const BridgeForm = ({ /> - {toAccount && ( + {toAccount && selectedRoute.externalProjectId !== "hyper-liquid" && ( ( @@ -15,15 +16,19 @@ type DepositStatusAnimatedIconsParams = { status: DepositStatus; fromChainId: number; toChainId: number; + externalProjectId?: string; }; const DepositStatusAnimatedIcons = ({ status, fromChainId, toChainId, + externalProjectId, }: DepositStatusAnimatedIconsParams) => { const GrayscaleLogoFromChain = getChainInfo(fromChainId).grayscaleLogoSvg; - const GrayscaleLogoToChain = getChainInfo(toChainId).grayscaleLogoSvg; + const GrayscaleLogoToChain = externalProjectId + ? externConfigs[externalProjectId].grayscaleLogoSvg + : getChainInfo(toChainId).grayscaleLogoSvg; return ( <> diff --git a/src/views/DepositStatus/components/DepositStatusLowerCard.tsx b/src/views/DepositStatus/components/DepositStatusLowerCard.tsx index 6f3c3e8ac..aeccc8936 100644 --- a/src/views/DepositStatus/components/DepositStatusLowerCard.tsx +++ b/src/views/DepositStatus/components/DepositStatusLowerCard.tsx @@ -20,6 +20,7 @@ import { EarnByLpAndStakingCard } from "./EarnByLpAndStakingCard"; type Props = { fromChainId: number; toChainId: number; + externalProjectId?: string; inputTokenSymbol: string; outputTokenSymbol: string; fromBridgePagePayload?: FromBridgePagePayload; @@ -28,6 +29,7 @@ type Props = { export function DepositStatusLowerCard({ fromChainId, toChainId, + externalProjectId, inputTokenSymbol, outputTokenSymbol, fromBridgePagePayload, @@ -103,6 +105,7 @@ export function DepositStatusLowerCard({ toChainId, inputTokenSymbol: baseToken.symbol, outputTokenSymbol, + externalProjectId, }) ) } diff --git a/src/views/DepositStatus/components/DepositStatusUpperCard.tsx b/src/views/DepositStatus/components/DepositStatusUpperCard.tsx index b9c1e40b9..e7ad8ad71 100644 --- a/src/views/DepositStatus/components/DepositStatusUpperCard.tsx +++ b/src/views/DepositStatus/components/DepositStatusUpperCard.tsx @@ -22,6 +22,7 @@ type Props = { depositTxHash: string; fromChainId: number; toChainId: number; + externalProjectId?: string; inputTokenSymbol: string; outputTokenSymbol?: string; fromBridgePagePayload?: FromBridgePagePayload; @@ -31,9 +32,10 @@ export function DepositStatusUpperCard({ depositTxHash, fromChainId, toChainId, - fromBridgePagePayload, + externalProjectId, inputTokenSymbol, outputTokenSymbol, + fromBridgePagePayload, }: Props) { const { depositQuery, fillQuery } = useDepositTracking( depositTxHash, @@ -94,6 +96,7 @@ export function DepositStatusUpperCard({ status={status} toChainId={toChainId} fromChainId={fromChainId} + externalProjectId={externalProjectId} /> {status === "filled" ? ( From 4d2880e3958b8208c159ccad5b285d70b4db29c4 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Thu, 9 Jan 2025 15:54:22 -0500 Subject: [PATCH 4/9] feat: add deposit tracking Signed-off-by: james-a-morris --- .../DepositsTable/cells/RouteCell.tsx | 10 ++++-- src/utils/hyperliquid.ts | 32 +++++++++++++++++++ src/utils/index.ts | 1 + src/views/Bridge/hooks/useBridgeAction.ts | 4 +-- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 src/utils/hyperliquid.ts diff --git a/src/components/DepositsTable/cells/RouteCell.tsx b/src/components/DepositsTable/cells/RouteCell.tsx index 5819b0391..e8ed1124e 100644 --- a/src/components/DepositsTable/cells/RouteCell.tsx +++ b/src/components/DepositsTable/cells/RouteCell.tsx @@ -3,9 +3,10 @@ import styled from "@emotion/styled"; import { Text } from "components/Text"; import { IconPair } from "components/IconPair"; import { Deposit } from "hooks/useDeposits"; -import { getChainInfo } from "utils"; +import { ChainInfo, getChainInfo, isHyperLiquidBoundDeposit } from "utils"; import { BaseCell } from "./BaseCell"; +import { externConfigs } from "constants/chains/configs"; type Props = { deposit: Deposit; @@ -13,8 +14,13 @@ type Props = { }; export function RouteCell({ deposit, width }: Props) { + const isHyperLiquidDeposit = isHyperLiquidBoundDeposit(deposit); + const sourceChain = getChainInfo(deposit.sourceChainId); - const destinationChain = getChainInfo(deposit.destinationChainId); + const destinationChain: Pick = + isHyperLiquidDeposit + ? externConfigs["hyper-liquid"] + : getChainInfo(deposit.destinationChainId); return ( diff --git a/src/utils/hyperliquid.ts b/src/utils/hyperliquid.ts new file mode 100644 index 000000000..990318a6c --- /dev/null +++ b/src/utils/hyperliquid.ts @@ -0,0 +1,32 @@ +import { Deposit } from "hooks/useDeposits"; +import { CHAIN_IDs } from "@across-protocol/constants"; +import { utils } from "ethers"; + +export function isHyperLiquidBoundDeposit(deposit: Deposit) { + if (deposit.destinationChainId !== CHAIN_IDs.ARBITRUM || !deposit.message) { + return false; + } + + try { + // Try to decode the message as Instructions struct + const decoded = utils.defaultAbiCoder.decode( + [ + "tuple(tuple(address target, bytes callData, uint256 value)[] calls, address fallbackRecipient)", + ], + deposit.message + ); + + // Check if it has exactly 2 calls + if (decoded[0].calls.length !== 2) { + return false; + } + + // Check if second call is to HyperLiquid Bridge2 contract + return ( + decoded[0].calls[1].target.toLowerCase() === + "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7".toLowerCase() + ); + } catch { + return false; + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 271e0c6c3..9031b1b66 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -22,3 +22,4 @@ export * from "./types"; export * from "./network"; export * from "./url"; export * from "./sdk"; +export * from "./hyperliquid"; diff --git a/src/views/Bridge/hooks/useBridgeAction.ts b/src/views/Bridge/hooks/useBridgeAction.ts index 1a792c150..83a5ad45a 100644 --- a/src/views/Bridge/hooks/useBridgeAction.ts +++ b/src/views/Bridge/hooks/useBridgeAction.ts @@ -125,8 +125,8 @@ export function useBridgeAction( // 4. We must construct a payload to send to HL's Bridge2 contract // 5. The user must sign this signature - // For now let's assume a 0.05% loss in the amount - const amount = frozenDepositArgs.amount.mul(9995).div(10000); + // For now let's assume a 2% loss in the amount + const amount = frozenDepositArgs.amount.mul(98).div(100); // Build the payload const hyperLiquidPayload = await generateHyperLiquidPayload( From 0a872bd4208936953b8abacf85320a46c1783985 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Fri, 10 Jan 2025 10:49:21 -0500 Subject: [PATCH 5/9] nit: naming change Signed-off-by: james-a-morris --- scripts/extern-configs/hyper-liquid/index.ts | 2 +- src/constants/chains/configs.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/extern-configs/hyper-liquid/index.ts b/scripts/extern-configs/hyper-liquid/index.ts index 4475023ca..a0fc67750 100644 --- a/scripts/extern-configs/hyper-liquid/index.ts +++ b/scripts/extern-configs/hyper-liquid/index.ts @@ -2,7 +2,7 @@ import { CHAIN_IDs } from "@across-protocol/constants"; import { ExternalProjectConfig } from "../types"; export default { - name: "Hyper Liquid", + name: "Hyperliquid", projectId: "hyper-liquid", explorer: "https://arbiscan.io", logoPath: "./assets/logo.svg", diff --git a/src/constants/chains/configs.ts b/src/constants/chains/configs.ts index 981387ef4..2e9bb6a3b 100644 --- a/src/constants/chains/configs.ts +++ b/src/constants/chains/configs.ts @@ -529,8 +529,8 @@ export const zora = { }; export const hyperLiquid = { - name: "Hyper Liquid", - fullName: "Hyper liquid", + name: "Hyperliquid", + fullName: "Hyperliquid", projectId: "hyper-liquid", logoURI: hyperLiquidLogo, grayscaleLogoURI: hyperLiquidGrayscaleLogo, From a691c70ff4f21a589663de1edc996e5a6457b579 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Fri, 10 Jan 2025 12:02:47 -0500 Subject: [PATCH 6/9] improve: hone in the fees Signed-off-by: james-a-morris --- src/views/Bridge/hooks/useBridgeAction.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/views/Bridge/hooks/useBridgeAction.ts b/src/views/Bridge/hooks/useBridgeAction.ts index 83a5ad45a..27a027fe3 100644 --- a/src/views/Bridge/hooks/useBridgeAction.ts +++ b/src/views/Bridge/hooks/useBridgeAction.ts @@ -125,8 +125,13 @@ export function useBridgeAction( // 4. We must construct a payload to send to HL's Bridge2 contract // 5. The user must sign this signature - // For now let's assume a 2% loss in the amount - const amount = frozenDepositArgs.amount.mul(98).div(100); + // Estimated fee for this HL deposit. Sum of the relayer capital fee, + // the lp fee, and 2x the relayer gas fee. + const estimatedFee = frozenFeeQuote.relayerCapitalFee.total + .add(frozenFeeQuote.lpFee.total) + .add(frozenFeeQuote.relayerGasFee.total.mul(2)); + + const amount = frozenDepositArgs.amount.sub(estimatedFee); // Build the payload const hyperLiquidPayload = await generateHyperLiquidPayload( From 518d1d131c947b0b46b54896293057c889d17838 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Fri, 10 Jan 2025 12:06:39 -0500 Subject: [PATCH 7/9] improve: increase floor Signed-off-by: james-a-morris --- src/views/Bridge/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/Bridge/utils.ts b/src/views/Bridge/utils.ts index 7fccc9c31..9eb18cfc6 100644 --- a/src/views/Bridge/utils.ts +++ b/src/views/Bridge/utils.ts @@ -130,7 +130,7 @@ export function validateBridgeAmount( quoteFees?.isAmountTooLow || // HyperLiquid has a minimum deposit amount of 5 USDC (selectedRoute.externalProjectId === "hyper-liquid" && - parsedAmountInput.lt(parseUnits("5", 6))) + parsedAmountInput.lt(parseUnits("5.05", 6))) ) { return { error: AmountInputError.AMOUNT_TOO_LOW, From 8b1346777fe14f0299b7c0b22e2ba8683450d2e0 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Fri, 10 Jan 2025 12:39:19 -0500 Subject: [PATCH 8/9] improve: use constants Signed-off-by: james-a-morris --- src/utils/constants.ts | 7 +++++++ src/utils/hyperliquid.ts | 8 +++++--- src/views/Bridge/hooks/useBridgeAction.ts | 8 +++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 49508b541..88c1a84e7 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -551,3 +551,10 @@ export const defaultSwapSlippage = Number( export const indexerApiBaseUrl = process.env.REACT_APP_INDEXER_BASE_URL || undefined; + +export const hyperLiquidBridge2Address = + "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7"; + +export const acrossPlusMulticallHandler: Record = { + [CHAIN_IDs.ARBITRUM]: "0x924a9f036260DdD5808007E1AA95f08eD08aA569", +}; diff --git a/src/utils/hyperliquid.ts b/src/utils/hyperliquid.ts index 990318a6c..2e75d1e25 100644 --- a/src/utils/hyperliquid.ts +++ b/src/utils/hyperliquid.ts @@ -1,6 +1,8 @@ import { Deposit } from "hooks/useDeposits"; import { CHAIN_IDs } from "@across-protocol/constants"; import { utils } from "ethers"; +import { compareAddressesSimple } from "./sdk"; +import { hyperLiquidBridge2Address } from "./constants"; export function isHyperLiquidBoundDeposit(deposit: Deposit) { if (deposit.destinationChainId !== CHAIN_IDs.ARBITRUM || !deposit.message) { @@ -22,9 +24,9 @@ export function isHyperLiquidBoundDeposit(deposit: Deposit) { } // Check if second call is to HyperLiquid Bridge2 contract - return ( - decoded[0].calls[1].target.toLowerCase() === - "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7".toLowerCase() + return compareAddressesSimple( + decoded[0].calls[1].target, + hyperLiquidBridge2Address ); } catch { return false; diff --git a/src/views/Bridge/hooks/useBridgeAction.ts b/src/views/Bridge/hooks/useBridgeAction.ts index 27a027fe3..7875d8a7f 100644 --- a/src/views/Bridge/hooks/useBridgeAction.ts +++ b/src/views/Bridge/hooks/useBridgeAction.ts @@ -25,6 +25,8 @@ import { sendSwapAndBridgeTx, compareAddressesSimple, getToken, + acrossPlusMulticallHandler, + hyperLiquidBridge2Address, } from "utils"; import { TransferQuote } from "./useTransferQuote"; import { SelectedRoute } from "../utils"; @@ -163,7 +165,7 @@ export function useBridgeAction( value: 0, }, { - target: "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7", // Bridge2 contract + target: hyperLiquidBridge2Address, callData: hyperLiquidPayload, value: 0, }, @@ -271,7 +273,7 @@ export function useBridgeAction( fillDeadline: frozenFeeQuote.fillDeadline, message: externalPayload, toAddress: externalProjectIsHyperLiquid - ? "0x924a9f036260DdD5808007E1AA95f08eD08aA569" // Default multicall handler + ? acrossPlusMulticallHandler[frozenRoute.toChain] : frozenDepositArgs.toAddress, }, spokePool, @@ -472,7 +474,7 @@ export async function generateHyperLiquidPayload( const permitValue = { owner: source, - spender: "0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7", // Bridge2 contract address + spender: hyperLiquidBridge2Address, value: amount, nonce: await usdcContract.nonces(source), deadline, From 7cadd8059dd3e14431d076ae30cc30093b2eaad2 Mon Sep 17 00:00:00 2001 From: james-a-morris Date: Fri, 10 Jan 2025 13:00:43 -0500 Subject: [PATCH 9/9] feat: add amplitude Signed-off-by: james-a-morris --- src/ampli/index.ts | 54 ++++++++++++++++++----- src/utils/amplitude.ts | 23 ++++++++-- src/views/Bridge/hooks/useBridgeAction.ts | 11 ++++- src/views/Bridge/hooks/useSelectRoute.ts | 17 +++++-- 4 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/ampli/index.ts b/src/ampli/index.ts index fa531a64b..df5300159 100644 --- a/src/ampli/index.ts +++ b/src/ampli/index.ts @@ -203,7 +203,7 @@ export interface ConnectWalletButtonClickedProperties { /** * | Rule | Value | * |---|---| - * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage | + * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage, marketingBlogSpecificPage, marketingBlogHomePage | */ page: | "splashPage" @@ -219,7 +219,9 @@ export interface ConnectWalletButtonClickedProperties { | "marketingBridgePage" | "marketingAcrossPlusPage" | "marketingSettlementPage" - | "depositStatusPage"; + | "depositStatusPage" + | "marketingBlogSpecificPage" + | "marketingBlogHomePage"; /** * | Rule | Value | * |---|---| @@ -246,7 +248,7 @@ export interface CtaButtonClickedProperties { /** * | Rule | Value | * |---|---| - * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage | + * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage, marketingBlogSpecificPage, marketingBlogHomePage | */ page: | "splashPage" @@ -262,7 +264,9 @@ export interface CtaButtonClickedProperties { | "marketingBridgePage" | "marketingAcrossPlusPage" | "marketingSettlementPage" - | "depositStatusPage"; + | "depositStatusPage" + | "marketingBlogSpecificPage" + | "marketingBlogHomePage"; /** * | Rule | Value | * |---|---| @@ -320,7 +324,7 @@ export interface DisconnectWalletButtonClickedProperties { /** * | Rule | Value | * |---|---| - * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage | + * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage, marketingBlogSpecificPage, marketingBlogHomePage | */ page: | "splashPage" @@ -336,7 +340,9 @@ export interface DisconnectWalletButtonClickedProperties { | "marketingBridgePage" | "marketingAcrossPlusPage" | "marketingSettlementPage" - | "depositStatusPage"; + | "depositStatusPage" + | "marketingBlogSpecificPage" + | "marketingBlogHomePage"; /** * | Rule | Value | * |---|---| @@ -385,7 +391,7 @@ export interface EarnByAddingLiquidityClickedProperties { /** * | Rule | Value | * |---|---| - * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage | + * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage, marketingBlogSpecificPage, marketingBlogHomePage | */ page: | "splashPage" @@ -401,7 +407,9 @@ export interface EarnByAddingLiquidityClickedProperties { | "marketingBridgePage" | "marketingAcrossPlusPage" | "marketingSettlementPage" - | "depositStatusPage"; + | "depositStatusPage" + | "marketingBlogSpecificPage" + | "marketingBlogHomePage"; /** * | Rule | Value | * |---|---| @@ -558,7 +566,7 @@ export interface PageViewedProperties { /** * | Rule | Value | * |---|---| - * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage | + * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage, marketingBlogSpecificPage, marketingBlogHomePage | */ page: | "splashPage" @@ -574,7 +582,9 @@ export interface PageViewedProperties { | "marketingBridgePage" | "marketingAcrossPlusPage" | "marketingSettlementPage" - | "depositStatusPage"; + | "depositStatusPage" + | "marketingBlogSpecificPage" + | "marketingBlogHomePage"; path: string; /** * Address of referee, null if no referral used @@ -616,7 +626,7 @@ export interface QuickSwapButtonClickedProperties { /** * | Rule | Value | * |---|---| - * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage | + * | Enum Values | splashPage, bridgePage, poolPage, rewardsPage, transactionsPage, stakingPage, referralPage, airdropPage, 404Page, marketingHomePage, marketingBridgePage, marketingAcrossPlusPage, marketingSettlementPage, depositStatusPage, marketingBlogSpecificPage, marketingBlogHomePage | */ page: | "splashPage" @@ -632,7 +642,9 @@ export interface QuickSwapButtonClickedProperties { | "marketingBridgePage" | "marketingAcrossPlusPage" | "marketingSettlementPage" - | "depositStatusPage"; + | "depositStatusPage" + | "marketingBlogSpecificPage" + | "marketingBlogHomePage"; /** * | Rule | Value | * |---|---| @@ -671,6 +683,12 @@ export interface ToChainSelectedProperties { * Whether or not this event is the default value loaded when an event is rendered. */ default?: boolean; + /** + * | Rule | Value | + * |---|---| + * | Enum Values | hyper-liquid | + */ + externalProjectId?: "hyper-liquid"; /** * Id of the toChain */ @@ -1117,6 +1135,12 @@ export interface TransferSignedProperties { * | Type | number | */ expectedFillTimeInMinutesUpperBound?: number; + /** + * | Rule | Value | + * |---|---| + * | Enum Values | hyper-liquid | + */ + externalProjectId?: "hyper-liquid"; /** * From amount in the bridge token, in decimals */ @@ -1294,6 +1318,12 @@ export interface TransferSubmittedProperties { * | Type | number | */ expectedFillTimeInMinutesUpperBound?: number; + /** + * | Rule | Value | + * |---|---| + * | Enum Values | hyper-liquid | + */ + externalProjectId?: "hyper-liquid"; /** * From amount in the bridge token, in decimals */ diff --git a/src/utils/amplitude.ts b/src/utils/amplitude.ts index d82646920..fc181a2c9 100644 --- a/src/utils/amplitude.ts +++ b/src/utils/amplitude.ts @@ -64,6 +64,8 @@ export const pageLookup: Record< ), }; +export type ExternalProjectId = "hyper-liquid"; + export function getPageValue() { // Resolve the sanitized pathname const path = getSanitizedPathname(); @@ -144,16 +146,27 @@ export function trackFromChainChanged(chainId: ChainId, isDefault?: boolean) { }); } -export function trackToChainChanged(chainId: ChainId, isDefault?: boolean) { +export function trackToChainChanged( + chainId: ChainId, + externalProjectId?: ExternalProjectId, + isDefault?: boolean +) { if (Number.isNaN(chainId)) return Promise.resolve(); const chain = getChainInfo(chainId); return ampli.toChainSelected({ toChainId: chain.chainId.toString(), chainName: chain.name, + externalProjectId, default: isDefault, }); } +export function externalProjectNameToId( + projectName?: string +): ExternalProjectId | undefined { + return projectName === "hyper-liquid" ? "hyper-liquid" : undefined; +} + export function trackQuickSwap( section: QuickSwapButtonClickedProperties["section"] ) { @@ -341,7 +354,8 @@ export function generateTransferQuote( export function generateTransferSubmitted( quote: TransferQuoteReceivedProperties, referralProgramAddress: string, - initialQuoteTime: number + initialQuoteTime: number, + externalProjectId?: string ): TransferSubmittedProperties { const { fromAddress, toAddress } = getConfig().getFromToAddressesBySymbol( quote.tokenSymbol, @@ -357,6 +371,7 @@ export function generateTransferSubmitted( ), transferTimestamp: String(Date.now()), toTokenAddress: toAddress, + externalProjectId: externalProjectNameToId(externalProjectId), }; } @@ -365,7 +380,8 @@ export function generateTransferSigned( quote: TransferQuoteReceivedProperties, referralProgramAddress: string, initialSubmissionTime: number, - txHash: string + txHash: string, + externalProjectId?: string ): TransferSignedProperties { const { fromAddress, toAddress } = getConfig().getFromToAddressesBySymbol( quote.tokenSymbol, @@ -381,6 +397,7 @@ export function generateTransferSigned( ), toTokenAddress: toAddress, transactionHash: txHash, + externalProjectId: externalProjectNameToId(externalProjectId), }; } diff --git a/src/views/Bridge/hooks/useBridgeAction.ts b/src/views/Bridge/hooks/useBridgeAction.ts index 7875d8a7f..b654281e5 100644 --- a/src/views/Bridge/hooks/useBridgeAction.ts +++ b/src/views/Bridge/hooks/useBridgeAction.ts @@ -27,6 +27,7 @@ import { getToken, acrossPlusMulticallHandler, hyperLiquidBridge2Address, + externalProjectNameToId, } from "utils"; import { TransferQuote } from "./useTransferQuote"; import { SelectedRoute } from "../utils"; @@ -216,7 +217,10 @@ export function useBridgeAction( generateTransferSubmitted( frozenQuoteForAnalytics, referrer, - frozenInitialQuoteTime + frozenInitialQuoteTime, + externalProjectIsHyperLiquid + ? externalProjectNameToId(frozenRoute.externalProjectId) + : undefined ) ); }); @@ -287,7 +291,10 @@ export function useBridgeAction( frozenQuoteForAnalytics, referrer, timeSubmitted, - tx.hash + tx.hash, + externalProjectIsHyperLiquid + ? externalProjectNameToId(frozenRoute.externalProjectId) + : undefined ) ); }); diff --git a/src/views/Bridge/hooks/useSelectRoute.ts b/src/views/Bridge/hooks/useSelectRoute.ts index f5fe31568..1153cf402 100644 --- a/src/views/Bridge/hooks/useSelectRoute.ts +++ b/src/views/Bridge/hooks/useSelectRoute.ts @@ -6,6 +6,7 @@ import { trackTokenChanged, trackQuickSwap, similarTokensMap, + externalProjectNameToId, } from "utils"; import { useAmplitude, useConnection } from "hooks"; @@ -43,7 +44,11 @@ export function useSelectRoute() { addToAmpliQueue(() => { trackTokenChanged(selectedRoute.fromTokenSymbol, true); trackFromChainChanged(selectedRoute.fromChain, true); - trackToChainChanged(selectedRoute.toChain, true); + trackToChainChanged( + selectedRoute.toChain, + externalProjectNameToId(selectedRoute.externalProjectId), + true + ); }); setIsDefaultRouteTracked(true); }, [selectedRoute, addToAmpliQueue, isDefaultRouteTracked]); @@ -280,7 +285,10 @@ export function useSelectRoute() { setSelectedRoute(route); addToAmpliQueue(() => { - trackToChainChanged(route.toChain); + trackToChainChanged( + route.toChain, + externalProjectNameToId(route.externalProjectId) + ); }); }, [selectedRoute, addToAmpliQueue] @@ -315,7 +323,10 @@ export function useSelectRoute() { addToAmpliQueue(() => { trackFromChainChanged(route.fromChain); - trackToChainChanged(route.toChain); + trackToChainChanged( + route.toChain, + externalProjectNameToId(route.externalProjectId) + ); trackQuickSwap("bridgeForm"); }); }