From 2efad0066c88a348ecdd5e9f13ef249db4e386eb Mon Sep 17 00:00:00 2001 From: Bartek Date: Wed, 15 Jan 2025 15:46:49 +0100 Subject: [PATCH 01/14] feat: add deBridge fast bridge (#2192) --- .../public/images/bridge/deBridge.svg | 13 +++++++++++++ .../TransferPanel/WithdrawalConfirmationDialog.tsx | 1 + .../src/components/common/BridgesTable.tsx | 2 +- .../arb-token-bridge-ui/src/util/fastBridges.ts | 12 +++++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 packages/arb-token-bridge-ui/public/images/bridge/deBridge.svg diff --git a/packages/arb-token-bridge-ui/public/images/bridge/deBridge.svg b/packages/arb-token-bridge-ui/public/images/bridge/deBridge.svg new file mode 100644 index 0000000000..783e2d23f9 --- /dev/null +++ b/packages/arb-token-bridge-ui/public/images/bridge/deBridge.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx index eda5f9edda..0acfeac024 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/WithdrawalConfirmationDialog.tsx @@ -64,6 +64,7 @@ export function WithdrawalConfirmationDialog( from: childChain.id, to: parentChain.id, tokenSymbol: selectedToken?.symbol ?? nativeCurrency.symbol, + tokenAddress: selectedToken?.address, amount: props.amount }) diff --git a/packages/arb-token-bridge-ui/src/components/common/BridgesTable.tsx b/packages/arb-token-bridge-ui/src/components/common/BridgesTable.tsx index 0049f2cf2c..3e99cb02a1 100644 --- a/packages/arb-token-bridge-ui/src/components/common/BridgesTable.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/BridgesTable.tsx @@ -56,7 +56,7 @@ export function BridgesTable(props: { return 1 } - if (a.name < b.name) { + if (a.name.toLowerCase() < b.name.toLowerCase()) { return -1 } else { return 1 diff --git a/packages/arb-token-bridge-ui/src/util/fastBridges.ts b/packages/arb-token-bridge-ui/src/util/fastBridges.ts index 45f17f6c77..51a10cdc04 100644 --- a/packages/arb-token-bridge-ui/src/util/fastBridges.ts +++ b/packages/arb-token-bridge-ui/src/util/fastBridges.ts @@ -9,6 +9,7 @@ import Synapse from '@/images/bridge/synapse.png' import Wormhole from '@/images/bridge/wormhole.svg' // import LIFI from '@/images/bridge/lifi.webp' import Router from '@/images/bridge/router.webp' +import deBridge from '@/images/bridge/deBridge.svg' import { ChainId } from '../types/ChainId' import { USDC_LEARN_MORE_LINK } from '../constants' @@ -22,7 +23,8 @@ export enum FastBridgeNames { Synapse = 'Synapse', Wormhole = 'Wormhole', LIFI = 'LI.FI', - Router = 'Router' + Router = 'Router', + deBridge = 'deBridge' } export enum SpecialTokenSymbol { @@ -39,11 +41,13 @@ export function getFastBridges({ from, to, tokenSymbol, + tokenAddress = '', amount }: { from: ChainId to: ChainId tokenSymbol: string + tokenAddress?: string amount: string }): FastBridgeInfo[] { function chainIdToSlug(chainId: ChainId): string { @@ -77,6 +81,8 @@ export function getFastBridges({ return `https://stargate.finance/transfer?srcChain=${chainIdToSlug( from )}&dstChain=${chainIdToSlug(to)}&srcToken=${tokenSymbol}` + case FastBridgeNames.deBridge: + return `https://app.debridge.finance/?inputChain=${from}&outputChain=${to}&amount=${amount}&inputCurrency=${tokenAddress}` case FastBridgeNames.Synapse: // We can't specify the input chain for Synapse, as it will use whatever the user is connected to. // We make sure to prompt a network switch to Arbitrum prior to showing this. @@ -118,6 +124,10 @@ export function getFastBridges({ [FastBridgeNames.Synapse]: { imageSrc: Synapse, href: getBridgeDeepLink(FastBridgeNames.Synapse) + }, + [FastBridgeNames.deBridge]: { + imageSrc: deBridge, + href: getBridgeDeepLink(FastBridgeNames.deBridge) } } From 23f05cfa7fe04d897337768f4463dd49db21e31b Mon Sep 17 00:00:00 2001 From: spsjvc Date: Wed, 15 Jan 2025 16:04:19 +0100 Subject: [PATCH 02/14] refactor: remove custom infura provider (#2193) --- .../arb-token-bridge-ui/src/util/infura.ts | 51 ------------------- .../src/util/wagmi/setup.ts | 2 - 2 files changed, 53 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/util/infura.ts b/packages/arb-token-bridge-ui/src/util/infura.ts index dfe3b4d48a..1f07c7de8b 100644 --- a/packages/arb-token-bridge-ui/src/util/infura.ts +++ b/packages/arb-token-bridge-ui/src/util/infura.ts @@ -1,56 +1,5 @@ -import { providers } from 'ethers' -import { Chain, ChainProviderFn } from 'wagmi' - import { ChainId } from '../types/ChainId' -// custom implementation based on https://github.com/wevm/wagmi/blob/wagmi%400.12.13/packages/core/src/providers/infura.ts -// with multiple infura keys support -export function customInfuraProvider(): ChainProviderFn< - TChain, - providers.InfuraProvider, - providers.InfuraWebSocketProvider -> { - return function (chain) { - // Retrieve the API key for the current chain's network - const infuraKey = chainIdToInfuraKey(chain.id) - - if (!infuraKey) return null - if (!chain.rpcUrls.infura?.http[0]) return null - - return { - chain: { - ...chain, - rpcUrls: { - ...chain.rpcUrls, - default: { - http: [`${chain.rpcUrls.infura.http[0]}/${infuraKey}`] - } - } - } as TChain, - provider: () => { - const provider = new providers.InfuraProvider( - { - chainId: chain.id, - name: chain.network, - ensAddress: chain.contracts?.ensRegistry?.address - }, - infuraKey - ) - return Object.assign(provider) - }, - webSocketProvider: () => - new providers.InfuraWebSocketProvider( - { - chainId: chain.id, - name: chain.network, - ensAddress: chain.contracts?.ensRegistry?.address - }, - infuraKey - ) - } - } -} - export function chainIdToInfuraKey(chainId: ChainId) { const defaultInfuraKey = process.env.NEXT_PUBLIC_INFURA_KEY diff --git a/packages/arb-token-bridge-ui/src/util/wagmi/setup.ts b/packages/arb-token-bridge-ui/src/util/wagmi/setup.ts index aded4842ab..d3ac1b799f 100644 --- a/packages/arb-token-bridge-ui/src/util/wagmi/setup.ts +++ b/packages/arb-token-bridge-ui/src/util/wagmi/setup.ts @@ -28,7 +28,6 @@ import { getCustomChainsFromLocalStorage, rpcURLs } from '../networks' import { ChainId } from '../../types/ChainId' import { getOrbitChains } from '../orbitChainsList' import { getWagmiChain } from './getWagmiChain' -import { customInfuraProvider } from '../infura' const customChains = getCustomChainsFromLocalStorage().map(chain => getWagmiChain(chain.chainId) @@ -161,7 +160,6 @@ export function getProps(targetChainKey: string | null) { // https://github.com/wagmi-dev/references/blob/main/packages/connectors/src/walletConnect.ts#L114 getChains(sanitizeTargetChainKey(targetChainKey)), [ - customInfuraProvider(), publicProvider(), jsonRpcProvider({ rpc: chain => ({ From 302b2be114f5e70cd18914f7500d584ae7ff5aba Mon Sep 17 00:00:00 2001 From: spsjvc Date: Thu, 16 Jan 2025 12:08:40 +0100 Subject: [PATCH 03/14] refactor: move nitro testnode networks to new file (#2194) --- .../arb-token-bridge-ui/src/util/networks.ts | 111 +----------------- .../src/util/networksNitroTestnode.ts | 107 +++++++++++++++++ .../arb-token-bridge-ui/synpress.config.ts | 6 +- .../tests/support/common.ts | 5 +- 4 files changed, 118 insertions(+), 111 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/util/networksNitroTestnode.ts diff --git a/packages/arb-token-bridge-ui/src/util/networks.ts b/packages/arb-token-bridge-ui/src/util/networks.ts index bb3fa0dbaa..dc1c74c8e6 100644 --- a/packages/arb-token-bridge-ui/src/util/networks.ts +++ b/packages/arb-token-bridge-ui/src/util/networks.ts @@ -1,4 +1,4 @@ -import { Provider, StaticJsonRpcProvider } from '@ethersproject/providers' +import { StaticJsonRpcProvider } from '@ethersproject/providers' import { ArbitrumNetwork, getChildrenForNetwork, @@ -13,6 +13,11 @@ import { chainIdToInfuraUrl } from './infura' import { fetchErc20Data } from './TokenUtils' import { orbitChains } from './orbitChainsList' import { ChainId } from '../types/ChainId' +import { + defaultL2Network, + defaultL3Network, + defaultL3CustomGasTokenNetwork +} from './networksNitroTestnode' /** The network that you reference when calling `block.number` in solidity */ type BlockNumberReferenceNetwork = { @@ -309,110 +314,6 @@ const defaultL1Network: BlockNumberReferenceNetwork = { isTestnet: true } -export const defaultL2Network: ArbitrumNetwork = { - chainId: 412346, - parentChainId: ChainId.Local, - confirmPeriodBlocks: 20, - ethBridge: { - bridge: '0x5eCF728ffC5C5E802091875f96281B5aeECf6C49', - inbox: '0x9f8c1c641336A371031499e3c362e40d58d0f254', - outbox: '0x50143333b44Ea46255BEb67255C9Afd35551072F', - rollup: process.env.NEXT_PUBLIC_IS_E2E_TEST - ? '0xE8A8F50F2a237D06D0087D14E690f6Ff0556259D' - : '0x46966d871d29e1772c2809459469f849d8AAb1A3', - sequencerInbox: '0x18d19C5d3E685f5be5b9C86E097f0E439285D216' - }, - isCustom: true, - isTestnet: true, - name: 'Arbitrum Local', - tokenBridge: { - parentCustomGateway: '0x8407E6180dC009D20D26D4BABB4790C1d4E6D2aA', - parentErc20Gateway: '0x00D9fE1a2B67B8151aEdE8855c95E58D73FB4245', - parentGatewayRouter: '0x093AAa96CD4387A68FC0e24C60140938Dc812549', - parentMultiCall: '0x49117fC32930E324F2E9A7BeA588FFb26008b8eC', - parentProxyAdmin: '0x2A1f38c9097e7883570e0b02BFBE6869Cc25d8a3', - parentWeth: '0x7E32b54800705876d3b5cFbc7d9c226a211F7C1a', - parentWethGateway: '0xB8F48Ba39fCfB44d70F6008fe1bf4F3E744044AF', - childCustomGateway: '0x0B35cfE62314C3852A0942b5830c728353BD654F', - childErc20Gateway: '0x7424e3DAAAAcd867c85ceB75c1E00119F2ee5eb7', - childGatewayRouter: '0x32656396981868E925280FB772b3f806892cf4bF', - childMultiCall: '0x6B1E93aE298B64e8f5b9f43B65Dd8F1eaA6DD4c3', - childProxyAdmin: '0x9F95547ABB0FfC92b4E37b3124d1e8613d5aB74A', - childWeth: '0xA1abD387192e3bb4e84D3109181F9f005aBaF5CA', - childWethGateway: '0x67aE8014BD1A0c1Ed747715d22b3b3a188aC324B' - } -} - -export const defaultL3Network: ArbitrumNetwork = { - chainId: 333333, - parentChainId: ChainId.ArbitrumLocal, - confirmPeriodBlocks: 20, - ethBridge: { - bridge: '0xA584795e24628D9c067A6480b033C9E96281fcA3', - inbox: '0xDcA690902d3154886Ec259308258D10EA5450996', - outbox: '0xda243bD61B011024FC923164db75Dde198AC6175', - rollup: process.env.NEXT_PUBLIC_IS_E2E_TEST - ? '0xdeD540257498027B1De7DFD4fe6cc4CeC030F355' - : '0xf9B0F86aCc3e42B7DF373c9a8adb2803BF0a7662', - sequencerInbox: '0x16c54EE2015CD824415c2077F4103f444E00A8cb' - }, - isCustom: true, - isTestnet: true, - name: 'L3 Local', - tokenBridge: { - parentCustomGateway: '0xA191D519260A06b32f8D04c84b9F457B8Caa0514', - parentErc20Gateway: '0x6B0805Fc6e275ef66a0901D0CE68805631E271e5', - parentGatewayRouter: '0xfE03DBdf7A126994dBd749631D7fbaB58C618c58', - parentMultiCall: '0x20a3627Dcc53756E38aE3F92717DE9B23617b422', - parentProxyAdmin: '0x1A61102c26ad3f64bA715B444C93388491fd8E68', - parentWeth: '0xA1abD387192e3bb4e84D3109181F9f005aBaF5CA', - parentWethGateway: '0x77603b0ea6a797C74Fa9ef11b5BdE04A4E03D550', - childCustomGateway: '0xD4816AeF8f85A3C1E01Cd071a81daD4fa941625f', - childErc20Gateway: '0xaa7d51aFFEeB32d99b1CB2fd6d81D7adA4a896e8', - childGatewayRouter: '0x8B6BC759226f8Fe687c8aD8Cc0DbF85E095e9297', - childMultiCall: '0x052B15c8Ff0544287AE689C4F2FC53A3905d7Db3', - childProxyAdmin: '0x36C56eC2CF3a3f53db9F01d0A5Ae84b36fb0A1e2', - childWeth: '0x582a8dBc77f665dF2c49Ce0a138978e9267dd968', - childWethGateway: '0xA6AB233B3c7bfd0399834897b5073974A3D467e2' - } -} - -export const defaultL3CustomGasTokenNetwork: ArbitrumNetwork = { - chainId: 333333, - parentChainId: ChainId.ArbitrumLocal, - confirmPeriodBlocks: 20, - ethBridge: { - bridge: '0xA584795e24628D9c067A6480b033C9E96281fcA3', - inbox: '0xDcA690902d3154886Ec259308258D10EA5450996', - outbox: '0xda243bD61B011024FC923164db75Dde198AC6175', - rollup: process.env.NEXT_PUBLIC_IS_E2E_TEST - ? '0x17d70d77AAEe46ACDF8b87BB2f085f36f63eC638' - : '0x7a23F33C1C384eFc11b8Cf207420c464ba2959CC', - sequencerInbox: '0x16c54EE2015CD824415c2077F4103f444E00A8cb' - }, - nativeToken: '0xE069078bA9ACCE4eeAE609d8754515Cf13dd6706', - isCustom: true, - isTestnet: true, - name: 'L3 Local', - retryableLifetimeSeconds: 604800, - tokenBridge: { - parentCustomGateway: '0xCe02eA568090ae7d5184B0a98df90f6aa69C1552', - parentErc20Gateway: '0x59156b0596689D965Ba707E160e5370AF22461a0', - parentGatewayRouter: '0x0C085152C2799834fc1603533ff6916fa1FdA302', - parentMultiCall: '0x20a3627Dcc53756E38aE3F92717DE9B23617b422', - parentProxyAdmin: '0x1A61102c26ad3f64bA715B444C93388491fd8E68', - parentWeth: '0xA1abD387192e3bb4e84D3109181F9f005aBaF5CA', - parentWethGateway: '0x59156b0596689D965Ba707E160e5370AF22461a0', - childCustomGateway: '0xD4816AeF8f85A3C1E01Cd071a81daD4fa941625f', - childErc20Gateway: '0xaa7d51aFFEeB32d99b1CB2fd6d81D7adA4a896e8', - childGatewayRouter: '0x8B6BC759226f8Fe687c8aD8Cc0DbF85E095e9297', - childMultiCall: '0x052B15c8Ff0544287AE689C4F2FC53A3905d7Db3', - childProxyAdmin: '0x36C56eC2CF3a3f53db9F01d0A5Ae84b36fb0A1e2', - childWeth: '0x0000000000000000000000000000000000000000', - childWethGateway: '0x0000000000000000000000000000000000000000' - } -} - export const localL1NetworkRpcUrl = loadEnvironmentVariableWithFallback({ env: process.env.NEXT_PUBLIC_RPC_URL_NITRO_TESTNODE_L1, fallback: 'http://127.0.0.1:8545' diff --git a/packages/arb-token-bridge-ui/src/util/networksNitroTestnode.ts b/packages/arb-token-bridge-ui/src/util/networksNitroTestnode.ts new file mode 100644 index 0000000000..800b9f9b14 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/networksNitroTestnode.ts @@ -0,0 +1,107 @@ +import { ArbitrumNetwork } from '@arbitrum/sdk' + +import { ChainId } from '../types/ChainId' + +export const defaultL2Network: ArbitrumNetwork = { + chainId: 412346, + parentChainId: ChainId.Local, + confirmPeriodBlocks: 20, + ethBridge: { + bridge: '0x5eCF728ffC5C5E802091875f96281B5aeECf6C49', + inbox: '0x9f8c1c641336A371031499e3c362e40d58d0f254', + outbox: '0x50143333b44Ea46255BEb67255C9Afd35551072F', + rollup: process.env.NEXT_PUBLIC_IS_E2E_TEST + ? '0xE8A8F50F2a237D06D0087D14E690f6Ff0556259D' + : '0x46966d871d29e1772c2809459469f849d8AAb1A3', + sequencerInbox: '0x18d19C5d3E685f5be5b9C86E097f0E439285D216' + }, + isCustom: true, + isTestnet: true, + name: 'Arbitrum Local', + tokenBridge: { + parentCustomGateway: '0x8407E6180dC009D20D26D4BABB4790C1d4E6D2aA', + parentErc20Gateway: '0x00D9fE1a2B67B8151aEdE8855c95E58D73FB4245', + parentGatewayRouter: '0x093AAa96CD4387A68FC0e24C60140938Dc812549', + parentMultiCall: '0x49117fC32930E324F2E9A7BeA588FFb26008b8eC', + parentProxyAdmin: '0x2A1f38c9097e7883570e0b02BFBE6869Cc25d8a3', + parentWeth: '0x7E32b54800705876d3b5cFbc7d9c226a211F7C1a', + parentWethGateway: '0xB8F48Ba39fCfB44d70F6008fe1bf4F3E744044AF', + childCustomGateway: '0x0B35cfE62314C3852A0942b5830c728353BD654F', + childErc20Gateway: '0x7424e3DAAAAcd867c85ceB75c1E00119F2ee5eb7', + childGatewayRouter: '0x32656396981868E925280FB772b3f806892cf4bF', + childMultiCall: '0x6B1E93aE298B64e8f5b9f43B65Dd8F1eaA6DD4c3', + childProxyAdmin: '0x9F95547ABB0FfC92b4E37b3124d1e8613d5aB74A', + childWeth: '0xA1abD387192e3bb4e84D3109181F9f005aBaF5CA', + childWethGateway: '0x67aE8014BD1A0c1Ed747715d22b3b3a188aC324B' + } +} + +export const defaultL3Network: ArbitrumNetwork = { + chainId: 333333, + parentChainId: ChainId.ArbitrumLocal, + confirmPeriodBlocks: 20, + ethBridge: { + bridge: '0xA584795e24628D9c067A6480b033C9E96281fcA3', + inbox: '0xDcA690902d3154886Ec259308258D10EA5450996', + outbox: '0xda243bD61B011024FC923164db75Dde198AC6175', + rollup: process.env.NEXT_PUBLIC_IS_E2E_TEST + ? '0xdeD540257498027B1De7DFD4fe6cc4CeC030F355' + : '0xf9B0F86aCc3e42B7DF373c9a8adb2803BF0a7662', + sequencerInbox: '0x16c54EE2015CD824415c2077F4103f444E00A8cb' + }, + isCustom: true, + isTestnet: true, + name: 'L3 Local', + tokenBridge: { + parentCustomGateway: '0xA191D519260A06b32f8D04c84b9F457B8Caa0514', + parentErc20Gateway: '0x6B0805Fc6e275ef66a0901D0CE68805631E271e5', + parentGatewayRouter: '0xfE03DBdf7A126994dBd749631D7fbaB58C618c58', + parentMultiCall: '0x20a3627Dcc53756E38aE3F92717DE9B23617b422', + parentProxyAdmin: '0x1A61102c26ad3f64bA715B444C93388491fd8E68', + parentWeth: '0xA1abD387192e3bb4e84D3109181F9f005aBaF5CA', + parentWethGateway: '0x77603b0ea6a797C74Fa9ef11b5BdE04A4E03D550', + childCustomGateway: '0xD4816AeF8f85A3C1E01Cd071a81daD4fa941625f', + childErc20Gateway: '0xaa7d51aFFEeB32d99b1CB2fd6d81D7adA4a896e8', + childGatewayRouter: '0x8B6BC759226f8Fe687c8aD8Cc0DbF85E095e9297', + childMultiCall: '0x052B15c8Ff0544287AE689C4F2FC53A3905d7Db3', + childProxyAdmin: '0x36C56eC2CF3a3f53db9F01d0A5Ae84b36fb0A1e2', + childWeth: '0x582a8dBc77f665dF2c49Ce0a138978e9267dd968', + childWethGateway: '0xA6AB233B3c7bfd0399834897b5073974A3D467e2' + } +} + +export const defaultL3CustomGasTokenNetwork: ArbitrumNetwork = { + chainId: 333333, + parentChainId: ChainId.ArbitrumLocal, + confirmPeriodBlocks: 20, + ethBridge: { + bridge: '0xA584795e24628D9c067A6480b033C9E96281fcA3', + inbox: '0xDcA690902d3154886Ec259308258D10EA5450996', + outbox: '0xda243bD61B011024FC923164db75Dde198AC6175', + rollup: process.env.NEXT_PUBLIC_IS_E2E_TEST + ? '0x17d70d77AAEe46ACDF8b87BB2f085f36f63eC638' + : '0x7a23F33C1C384eFc11b8Cf207420c464ba2959CC', + sequencerInbox: '0x16c54EE2015CD824415c2077F4103f444E00A8cb' + }, + nativeToken: '0xE069078bA9ACCE4eeAE609d8754515Cf13dd6706', + isCustom: true, + isTestnet: true, + name: 'L3 Local', + retryableLifetimeSeconds: 604800, + tokenBridge: { + parentCustomGateway: '0xCe02eA568090ae7d5184B0a98df90f6aa69C1552', + parentErc20Gateway: '0x59156b0596689D965Ba707E160e5370AF22461a0', + parentGatewayRouter: '0x0C085152C2799834fc1603533ff6916fa1FdA302', + parentMultiCall: '0x20a3627Dcc53756E38aE3F92717DE9B23617b422', + parentProxyAdmin: '0x1A61102c26ad3f64bA715B444C93388491fd8E68', + parentWeth: '0xA1abD387192e3bb4e84D3109181F9f005aBaF5CA', + parentWethGateway: '0x59156b0596689D965Ba707E160e5370AF22461a0', + childCustomGateway: '0xD4816AeF8f85A3C1E01Cd071a81daD4fa941625f', + childErc20Gateway: '0xaa7d51aFFEeB32d99b1CB2fd6d81D7adA4a896e8', + childGatewayRouter: '0x8B6BC759226f8Fe687c8aD8Cc0DbF85E095e9297', + childMultiCall: '0x052B15c8Ff0544287AE689C4F2FC53A3905d7Db3', + childProxyAdmin: '0x36C56eC2CF3a3f53db9F01d0A5Ae84b36fb0A1e2', + childWeth: '0x0000000000000000000000000000000000000000', + childWethGateway: '0x0000000000000000000000000000000000000000' + } +} diff --git a/packages/arb-token-bridge-ui/synpress.config.ts b/packages/arb-token-bridge-ui/synpress.config.ts index da2deeeae7..17a0636f06 100644 --- a/packages/arb-token-bridge-ui/synpress.config.ts +++ b/packages/arb-token-bridge-ui/synpress.config.ts @@ -29,12 +29,12 @@ import { getNativeTokenDecimals } from './tests/support/common' +import { registerLocalNetwork } from './src/util/networks' import { defaultL2Network, defaultL3Network, - defaultL3CustomGasTokenNetwork, - registerLocalNetwork -} from './src/util/networks' + defaultL3CustomGasTokenNetwork +} from './src/util/networksNitroTestnode' import { getCommonSynpressConfig } from './tests/e2e/getCommonSynpressConfig' const tests = process.env.TEST_FILE diff --git a/packages/arb-token-bridge-ui/tests/support/common.ts b/packages/arb-token-bridge-ui/tests/support/common.ts index 83aa73c988..807dda72b9 100644 --- a/packages/arb-token-bridge-ui/tests/support/common.ts +++ b/packages/arb-token-bridge-ui/tests/support/common.ts @@ -10,8 +10,7 @@ import { defaultL2Network, defaultL3Network, defaultL3CustomGasTokenNetwork -} from '../../src/util/networks' -import { getChainIdFromProvider } from '../../src/token-bridge-sdk/utils' +} from '../../src/util/networksNitroTestnode' export type NetworkType = 'parentChain' | 'childChain' export type NetworkName = @@ -276,7 +275,7 @@ export async function checkForAssertions({ const rollupContract = new ethers.Contract(rollupAddress, abi, parentProvider) - const parentChainId = await getChainIdFromProvider(parentProvider) + const parentChainId = (await parentProvider.getNetwork()).chainId try { while (true) { From 5c032feffa589f4e217d757a88e96888c07b7399 Mon Sep 17 00:00:00 2001 From: spsjvc Date: Thu, 16 Jan 2025 15:58:51 +0100 Subject: [PATCH 04/14] feat: improve rpc configuration (#2107) --- README.md | 9 +- .../arb-token-bridge-ui/.env.local.sample | 19 ++++ .../docs/rpc-configuration.md | 59 +++++++++++++ .../src/generateOpenGraphImages.tsx | 7 ++ .../arb-token-bridge-ui/src/util/infura.ts | 46 ---------- .../arb-token-bridge-ui/src/util/networks.ts | 40 +++++---- .../src/util/rpc/alchemy.test.ts | 35 ++++++++ .../src/util/rpc/alchemy.ts | 36 ++++++++ .../src/util/rpc/getRpcUrl.test.ts | 26 ++++++ .../src/util/rpc/getRpcUrl.ts | 49 +++++++++++ .../src/util/rpc/infura.test.ts | 32 +++++++ .../src/util/rpc/infura.ts | 87 +++++++++++++++++++ 12 files changed, 375 insertions(+), 70 deletions(-) create mode 100644 packages/arb-token-bridge-ui/docs/rpc-configuration.md delete mode 100644 packages/arb-token-bridge-ui/src/util/infura.ts create mode 100644 packages/arb-token-bridge-ui/src/util/rpc/alchemy.test.ts create mode 100644 packages/arb-token-bridge-ui/src/util/rpc/alchemy.ts create mode 100644 packages/arb-token-bridge-ui/src/util/rpc/getRpcUrl.test.ts create mode 100644 packages/arb-token-bridge-ui/src/util/rpc/getRpcUrl.ts create mode 100644 packages/arb-token-bridge-ui/src/util/rpc/infura.test.ts create mode 100644 packages/arb-token-bridge-ui/src/util/rpc/infura.ts diff --git a/README.md b/README.md index a57218f6fc..a478460cbe 100644 --- a/README.md +++ b/README.md @@ -69,14 +69,9 @@ Interested in contributing to this repo? We welcome your contribution. 2. In `.env` created, add `NEXT_PUBLIC_INFURA_KEY=my-infura-key` - 3. Set `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` to your WalletConnect project ID. You can create a new project on the [WalletConnect dashboard](https://cloud.walletconnect.com/app). + 3. (Optional) If you want to use a different RPC provider or your own RPC, please see [RPC Configuration](./packages/arb-token-bridge-ui/docs/rpc-configuration.md). - 4. For custom urls, set optional vars: - - - `NEXT_PUBLIC_RPC_URL_ETHEREUM=my-eth-node` - - `NEXT_PUBLIC_RPC_URL_SEPOLIA=my-sepolia-node` - (see [.env.local.sample](./packages/arb-token-bridge-ui/.env.local.sample)) - If no custom URL is provided, Infura will be used by default. + 4. Set `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID` to your WalletConnect project ID. You can create a new project on the [WalletConnect dashboard](https://cloud.walletconnect.com/app). 5. Build the project and internal packages diff --git a/packages/arb-token-bridge-ui/.env.local.sample b/packages/arb-token-bridge-ui/.env.local.sample index bb2d9a54f5..e08532daaf 100644 --- a/packages/arb-token-bridge-ui/.env.local.sample +++ b/packages/arb-token-bridge-ui/.env.local.sample @@ -1,3 +1,8 @@ +# ----- RPC Provider ----- + +# Used for selecting between Infura and Alchemy. See "/docs/rpc-configuration.md" for more details. +NEXT_PUBLIC_RPC_PROVIDER=infura + # ----- Infura ----- # Default Infura key. If no network-specific keys are set, it will fallback to this key. @@ -18,6 +23,10 @@ NEXT_PUBLIC_INFURA_KEY_BASE= NEXT_PUBLIC_INFURA_KEY_ARBITRUM_SEPOLIA= NEXT_PUBLIC_INFURA_KEY_BASE_SEPOLIA= +# ----- Alchemy ----- + +NEXT_PUBLIC_ALCHEMY_KEY= + # ----- Custom RPCs ----- # L1 Mainnet @@ -25,6 +34,16 @@ NEXT_PUBLIC_RPC_URL_ETHEREUM= # L1 Testnet NEXT_PUBLIC_RPC_URL_SEPOLIA= +NEXT_PUBLIC_RPC_URL_HOLESKY= + +# L2 Mainnet +NEXT_PUBLIC_RPC_URL_ARBITRUM_ONE= +NEXT_PUBLIC_RPC_URL_ARBITRUM_NOVA= +NEXT_PUBLIC_RPC_URL_BASE= + +# L2 Testnet +NEXT_PUBLIC_RPC_URL_ARBITRUM_SEPOLIA= +NEXT_PUBLIC_RPC_URL_BASE_SEPOLIA= # Nitro Testnode NEXT_PUBLIC_RPC_URL_NITRO_TESTNODE_L1="http://127.0.0.1:8545" diff --git a/packages/arb-token-bridge-ui/docs/rpc-configuration.md b/packages/arb-token-bridge-ui/docs/rpc-configuration.md new file mode 100644 index 0000000000..9433a03e07 --- /dev/null +++ b/packages/arb-token-bridge-ui/docs/rpc-configuration.md @@ -0,0 +1,59 @@ +# RPC Configuration + +This document outlines how configuring RPCs works, either through [RPC providers](#rpc-providers), or [custom RPCs](#custom-rpcs). + +## RPC Providers + +Two RPC providers are currently supported: [Infura](#infura) and [Alchemy](#alchemy). + +Selecting which one to use is done via the following environment variable: + +- `NEXT_PUBLIC_RPC_PROVIDER` + +The accepted values (case insensitive) are `infura` and `alchemy`. The default is `infura`. + +### Infura + +The Infura key is provided through the following environment variable: + +- `NEXT_PUBLIC_INFURA_KEY` + +The key will be used for all chains. However, if you need to have different keys for each chain, you can do so through the following environment variables: + +- `NEXT_PUBLIC_INFURA_KEY_ETHEREUM` +- `NEXT_PUBLIC_INFURA_KEY_SEPOLIA` +- `NEXT_PUBLIC_INFURA_KEY_HOLESKY` +- `NEXT_PUBLIC_INFURA_KEY_ARBITRUM_ONE` +- `NEXT_PUBLIC_INFURA_KEY_BASE` +- `NEXT_PUBLIC_INFURA_KEY_ARBITRUM_SEPOLIA` +- `NEXT_PUBLIC_INFURA_KEY_BASE_SEPOLIA` + +> [!NOTE] +> Arbitrum Nova is currently not supported on Infura, so the [public RPC](https://nova.arbitrum.io/rpc) will be used instead. + +### Alchemy + +The Alchemy key is provided through the following environment variable: + +- `NEXT_PUBLIC_ALCHEMY_KEY` + +The key will be used for all chains. + +## Custom RPCs + +If you need to override the RPC for a chain, you can do so through the following environment variables: + +- `NEXT_PUBLIC_RPC_URL_ETHEREUM` +- `NEXT_PUBLIC_RPC_URL_SEPOLIA` +- `NEXT_PUBLIC_RPC_URL_HOLESKY` +- `NEXT_PUBLIC_RPC_URL_ARBITRUM_ONE` +- `NEXT_PUBLIC_RPC_URL_ARBITRUM_NOVA` +- `NEXT_PUBLIC_RPC_URL_BASE` +- `NEXT_PUBLIC_RPC_URL_ARBITRUM_SEPOLIA` +- `NEXT_PUBLIC_RPC_URL_BASE_SEPOLIA` + +For [Nitro Testnode](https://github.com/OffchainLabs/nitro-testnode) chains: + +- `NEXT_PUBLIC_RPC_URL_NITRO_TESTNODE_L1` +- `NEXT_PUBLIC_RPC_URL_NITRO_TESTNODE_L2` +- `NEXT_PUBLIC_RPC_URL_NITRO_TESTNODE_L3` diff --git a/packages/arb-token-bridge-ui/src/generateOpenGraphImages.tsx b/packages/arb-token-bridge-ui/src/generateOpenGraphImages.tsx index 01ff07ff39..0c2a97c2bd 100644 --- a/packages/arb-token-bridge-ui/src/generateOpenGraphImages.tsx +++ b/packages/arb-token-bridge-ui/src/generateOpenGraphImages.tsx @@ -2,6 +2,13 @@ import React from 'react' import satori, { Font } from 'satori' import sharp from 'sharp' import fs from 'fs' +import path from 'path' +import dotenv from 'dotenv' + +// this has to be called before import from "networks.ts" +// to ensure that the environment variables are loaded +dotenv.config({ path: path.resolve(__dirname, '../.env') }) + import { isNetwork } from './util/networks' import { ChainId } from './types/ChainId' import { getBridgeUiConfigForChain } from './util/bridgeUiConfig' diff --git a/packages/arb-token-bridge-ui/src/util/infura.ts b/packages/arb-token-bridge-ui/src/util/infura.ts deleted file mode 100644 index 1f07c7de8b..0000000000 --- a/packages/arb-token-bridge-ui/src/util/infura.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ChainId } from '../types/ChainId' - -export function chainIdToInfuraKey(chainId: ChainId) { - const defaultInfuraKey = process.env.NEXT_PUBLIC_INFURA_KEY - - switch (chainId) { - case ChainId.Ethereum: - return process.env.NEXT_PUBLIC_INFURA_KEY_ETHEREUM || defaultInfuraKey - case ChainId.Sepolia: - return process.env.NEXT_PUBLIC_INFURA_KEY_SEPOLIA || defaultInfuraKey - case ChainId.ArbitrumOne: - return process.env.NEXT_PUBLIC_INFURA_KEY_ARBITRUM_ONE || defaultInfuraKey - case ChainId.Base: - return process.env.NEXT_PUBLIC_INFURA_KEY_BASE || defaultInfuraKey - case ChainId.ArbitrumSepolia: - return ( - process.env.NEXT_PUBLIC_INFURA_KEY_ARBITRUM_SEPOLIA || defaultInfuraKey - ) - case ChainId.BaseSepolia: - return process.env.NEXT_PUBLIC_INFURA_KEY_BASE_SEPOLIA || defaultInfuraKey - - default: - return defaultInfuraKey - } -} - -export function chainIdToInfuraUrl(chainId: ChainId) { - const infuraKey = chainIdToInfuraKey(chainId) - - switch (chainId) { - case ChainId.Ethereum: - return `https://mainnet.infura.io/v3/${infuraKey}` - case ChainId.Sepolia: - return `https://sepolia.infura.io/v3/${infuraKey}` - case ChainId.ArbitrumOne: - return `https://arbitrum-mainnet.infura.io/v3/${infuraKey}` - case ChainId.Base: - return `https://base-mainnet.infura.io/v3/${infuraKey}` - case ChainId.ArbitrumSepolia: - return `https://arbitrum-sepolia.infura.io/v3/${infuraKey}` - case ChainId.BaseSepolia: - return `https://base-sepolia.infura.io/v3/${infuraKey}` - default: - return undefined - } -} diff --git a/packages/arb-token-bridge-ui/src/util/networks.ts b/packages/arb-token-bridge-ui/src/util/networks.ts index dc1c74c8e6..1268747ed6 100644 --- a/packages/arb-token-bridge-ui/src/util/networks.ts +++ b/packages/arb-token-bridge-ui/src/util/networks.ts @@ -9,10 +9,10 @@ import { import { loadEnvironmentVariableWithFallback } from './index' import { getBridgeUiConfigForChain } from './bridgeUiConfig' -import { chainIdToInfuraUrl } from './infura' import { fetchErc20Data } from './TokenUtils' import { orbitChains } from './orbitChainsList' import { ChainId } from '../types/ChainId' +import { getRpcUrl } from './rpc/getRpcUrl' import { defaultL2Network, defaultL3Network, @@ -208,35 +208,41 @@ export const supportedCustomOrbitParentChains = [ ] export const rpcURLs: { [chainId: number]: string } = { - // L1 + // L1 Mainnet [ChainId.Ethereum]: loadEnvironmentVariableWithFallback({ env: process.env.NEXT_PUBLIC_RPC_URL_ETHEREUM, - fallback: chainIdToInfuraUrl(ChainId.Ethereum) + fallback: getRpcUrl(ChainId.Ethereum) }), - // L1 Testnets + // L1 Testnet [ChainId.Sepolia]: loadEnvironmentVariableWithFallback({ env: process.env.NEXT_PUBLIC_RPC_URL_SEPOLIA, - fallback: chainIdToInfuraUrl(ChainId.Sepolia) + fallback: getRpcUrl(ChainId.Sepolia) }), - [ChainId.Holesky]: 'https://ethereum-holesky-rpc.publicnode.com', - // L2 + [ChainId.Holesky]: loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_RPC_URL_HOLESKY, + fallback: getRpcUrl(ChainId.Holesky) + }), + // L2 Mainnet [ChainId.ArbitrumOne]: loadEnvironmentVariableWithFallback({ - env: chainIdToInfuraUrl(ChainId.ArbitrumOne), - fallback: 'https://arb1.arbitrum.io/rpc' + env: process.env.NEXT_PUBLIC_RPC_URL_ARBITRUM_ONE, + fallback: getRpcUrl(ChainId.ArbitrumOne) + }), + [ChainId.ArbitrumNova]: loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_RPC_URL_ARBITRUM_NOVA, + fallback: getRpcUrl(ChainId.ArbitrumNova) }), - [ChainId.ArbitrumNova]: 'https://nova.arbitrum.io/rpc', [ChainId.Base]: loadEnvironmentVariableWithFallback({ - env: chainIdToInfuraUrl(ChainId.Base), - fallback: 'https://mainnet.base.org' + env: process.env.NEXT_PUBLIC_RPC_URL_BASE, + fallback: getRpcUrl(ChainId.Base) }), - // L2 Testnets + // L2 Testnet [ChainId.ArbitrumSepolia]: loadEnvironmentVariableWithFallback({ - env: chainIdToInfuraUrl(ChainId.ArbitrumSepolia), - fallback: 'https://sepolia-rollup.arbitrum.io/rpc' + env: process.env.NEXT_PUBLIC_RPC_URL_ARBITRUM_SEPOLIA, + fallback: getRpcUrl(ChainId.ArbitrumSepolia) }), [ChainId.BaseSepolia]: loadEnvironmentVariableWithFallback({ - env: chainIdToInfuraUrl(ChainId.BaseSepolia), - fallback: 'https://sepolia.base.org' + env: process.env.NEXT_PUBLIC_RPC_URL_BASE_SEPOLIA, + fallback: getRpcUrl(ChainId.BaseSepolia) }) } diff --git a/packages/arb-token-bridge-ui/src/util/rpc/alchemy.test.ts b/packages/arb-token-bridge-ui/src/util/rpc/alchemy.test.ts new file mode 100644 index 0000000000..575812c1f5 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/rpc/alchemy.test.ts @@ -0,0 +1,35 @@ +import { ChainId } from '../../types/ChainId' +import { ProductionChainId } from './getRpcUrl' +import { getAlchemyRpcUrl } from './alchemy' + +it('successfully returns the correct url for the provided chain and key', () => { + const key = '123456' + + const result: { [Key in ProductionChainId]: string } = { + // L1 Mainnet + [ChainId.Ethereum]: getAlchemyRpcUrl(ChainId.Ethereum, key), + // L1 Testnet + [ChainId.Sepolia]: getAlchemyRpcUrl(ChainId.Sepolia, key), + [ChainId.Holesky]: getAlchemyRpcUrl(ChainId.Holesky, key), + // L2 Mainnet + [ChainId.ArbitrumOne]: getAlchemyRpcUrl(ChainId.ArbitrumOne, key), + [ChainId.ArbitrumNova]: getAlchemyRpcUrl(ChainId.ArbitrumNova, key), + [ChainId.Base]: getAlchemyRpcUrl(ChainId.Base, key), + // L2 Testnet + [ChainId.ArbitrumSepolia]: getAlchemyRpcUrl(ChainId.ArbitrumSepolia, key), + [ChainId.BaseSepolia]: getAlchemyRpcUrl(ChainId.BaseSepolia, key) + } + + expect(result).toMatchInlineSnapshot(` + { + "1": "https://eth-mainnet.g.alchemy.com/v2/123456", + "11155111": "https://eth-sepolia.g.alchemy.com/v2/123456", + "17000": "https://eth-holesky.g.alchemy.com/v2/123456", + "42161": "https://arb-mainnet.g.alchemy.com/v2/123456", + "421614": "https://arb-sepolia.g.alchemy.com/v2/123456", + "42170": "https://arbnova-mainnet.g.alchemy.com/v2/123456", + "8453": "https://base-mainnet.g.alchemy.com/v2/123456", + "84532": "https://base-sepolia.g.alchemy.com/v2/123456", + } + `) +}) diff --git a/packages/arb-token-bridge-ui/src/util/rpc/alchemy.ts b/packages/arb-token-bridge-ui/src/util/rpc/alchemy.ts new file mode 100644 index 0000000000..accebd35af --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/rpc/alchemy.ts @@ -0,0 +1,36 @@ +import { loadEnvironmentVariableWithFallback } from '..' +import { ChainId } from '../../types/ChainId' +import { ProductionChainId } from './getRpcUrl' + +export function getAlchemyRpcUrl( + chainId: ProductionChainId, + alchemyKey: string = loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_ALCHEMY_KEY + }) +): string { + switch (chainId) { + // L1 Mainnet + case ChainId.Ethereum: + return `https://eth-mainnet.g.alchemy.com/v2/${alchemyKey}` + + // L1 Testnet + case ChainId.Sepolia: + return `https://eth-sepolia.g.alchemy.com/v2/${alchemyKey}` + case ChainId.Holesky: + return `https://eth-holesky.g.alchemy.com/v2/${alchemyKey}` + + // L2 Mainnet + case ChainId.ArbitrumOne: + return `https://arb-mainnet.g.alchemy.com/v2/${alchemyKey}` + case ChainId.ArbitrumNova: + return `https://arbnova-mainnet.g.alchemy.com/v2/${alchemyKey}` + case ChainId.Base: + return `https://base-mainnet.g.alchemy.com/v2/${alchemyKey}` + + // L2 Testnet + case ChainId.ArbitrumSepolia: + return `https://arb-sepolia.g.alchemy.com/v2/${alchemyKey}` + case ChainId.BaseSepolia: + return `https://base-sepolia.g.alchemy.com/v2/${alchemyKey}` + } +} diff --git a/packages/arb-token-bridge-ui/src/util/rpc/getRpcUrl.test.ts b/packages/arb-token-bridge-ui/src/util/rpc/getRpcUrl.test.ts new file mode 100644 index 0000000000..2f01662b03 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/rpc/getRpcUrl.test.ts @@ -0,0 +1,26 @@ +import { getRpcUrl } from './getRpcUrl' +import { ChainId } from '../../types/ChainId' + +it('returns correct rpc for ethereum mainnet (infura)', () => { + expect(getRpcUrl(ChainId.Ethereum, 'infura', 'infura-key')).toEqual( + 'https://mainnet.infura.io/v3/infura-key' + ) +}) + +it('returns correct rpc for arbitrum nova (infura)', () => { + expect(getRpcUrl(ChainId.ArbitrumNova, 'infura', 'infura-key')).toEqual( + 'https://nova.arbitrum.io/rpc' + ) +}) + +it('returns correct rpc for ethereum mainnet (alchemy)', () => { + expect(getRpcUrl(ChainId.Ethereum, 'alchemy', 'alchemy-key')).toEqual( + 'https://eth-mainnet.g.alchemy.com/v2/alchemy-key' + ) +}) + +it('returns correct rpc for arbitrum nova (alchemy)', () => { + expect(getRpcUrl(ChainId.ArbitrumNova, 'alchemy', 'alchemy-key')).toEqual( + 'https://arbnova-mainnet.g.alchemy.com/v2/alchemy-key' + ) +}) diff --git a/packages/arb-token-bridge-ui/src/util/rpc/getRpcUrl.ts b/packages/arb-token-bridge-ui/src/util/rpc/getRpcUrl.ts new file mode 100644 index 0000000000..21fa7dedd6 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/rpc/getRpcUrl.ts @@ -0,0 +1,49 @@ +import { ChainId } from '../../types/ChainId' + +import { getInfuraRpcUrl } from './infura' +import { getAlchemyRpcUrl } from './alchemy' + +type RpcProvider = 'infura' | 'alchemy' + +export type ProductionChainId = Exclude< + ChainId, + ChainId.Local | ChainId.ArbitrumLocal | ChainId.L3Local +> + +function getRpcProvider(): RpcProvider { + const rpcProviderFromEnv = process.env.NEXT_PUBLIC_RPC_PROVIDER + + if (typeof rpcProviderFromEnv === 'undefined' || rpcProviderFromEnv === '') { + console.warn(`[getRpcProvider] no provider specified`) + console.warn(`[getRpcProvider] defaulting to infura`) + return 'infura' + } + + if (rpcProviderFromEnv !== 'infura' && rpcProviderFromEnv !== 'alchemy') { + console.warn(`[getRpcProvider] unknown provider "${rpcProviderFromEnv}"`) + console.warn(`[getRpcProvider] defaulting to infura`) + return 'infura' + } + + return rpcProviderFromEnv +} + +export function getRpcUrl( + chainId: ProductionChainId, + rpcProvider: RpcProvider = getRpcProvider(), + rpcProviderKey?: string +): string { + switch (rpcProvider) { + case 'infura': { + // only arbitrum nova is currently not supported on infura + if (chainId === ChainId.ArbitrumNova) { + return 'https://nova.arbitrum.io/rpc' + } + + return getInfuraRpcUrl(chainId, rpcProviderKey) + } + + case 'alchemy': + return getAlchemyRpcUrl(chainId, rpcProviderKey) + } +} diff --git a/packages/arb-token-bridge-ui/src/util/rpc/infura.test.ts b/packages/arb-token-bridge-ui/src/util/rpc/infura.test.ts new file mode 100644 index 0000000000..5d78978e44 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/rpc/infura.test.ts @@ -0,0 +1,32 @@ +import { ChainId } from '../../types/ChainId' +import { getInfuraRpcUrl, InfuraSupportedChainId } from './infura' + +it('successfully returns the correct url for the provided chain and key', () => { + const key = '123456' + + const result: { [Key in InfuraSupportedChainId]: string } = { + // L1 Mainnet + [ChainId.Ethereum]: getInfuraRpcUrl(ChainId.Ethereum, key), + // L1 Testnet + [ChainId.Sepolia]: getInfuraRpcUrl(ChainId.Sepolia, key), + [ChainId.Holesky]: getInfuraRpcUrl(ChainId.Holesky, key), + // L2 Mainnet + [ChainId.ArbitrumOne]: getInfuraRpcUrl(ChainId.ArbitrumOne, key), + [ChainId.Base]: getInfuraRpcUrl(ChainId.Base, key), + // L2 Testnet + [ChainId.ArbitrumSepolia]: getInfuraRpcUrl(ChainId.ArbitrumSepolia, key), + [ChainId.BaseSepolia]: getInfuraRpcUrl(ChainId.BaseSepolia, key) + } + + expect(result).toMatchInlineSnapshot(` + { + "1": "https://mainnet.infura.io/v3/123456", + "11155111": "https://sepolia.infura.io/v3/123456", + "17000": "https://holesky.infura.io/v3/123456", + "42161": "https://arbitrum-mainnet.infura.io/v3/123456", + "421614": "https://arbitrum-sepolia.infura.io/v3/123456", + "8453": "https://base-mainnet.infura.io/v3/123456", + "84532": "https://base-sepolia.infura.io/v3/123456", + } + `) +}) diff --git a/packages/arb-token-bridge-ui/src/util/rpc/infura.ts b/packages/arb-token-bridge-ui/src/util/rpc/infura.ts new file mode 100644 index 0000000000..1fd5678512 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/util/rpc/infura.ts @@ -0,0 +1,87 @@ +import { loadEnvironmentVariableWithFallback } from '..' +import { ChainId } from '../../types/ChainId' +import { ProductionChainId } from './getRpcUrl' + +export type InfuraSupportedChainId = Exclude< + ProductionChainId, + // only arbitrum nova is currently not supported on infura + ChainId.ArbitrumNova +> + +export function getInfuraKeyFromEnv(chainId: InfuraSupportedChainId): string { + const defaultInfuraKey = process.env.NEXT_PUBLIC_INFURA_KEY + + switch (chainId) { + // L1 Mainnet + case ChainId.Ethereum: + return loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_INFURA_KEY_ETHEREUM, + fallback: defaultInfuraKey + }) + + // L1 Testnet + case ChainId.Sepolia: + return loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_INFURA_KEY_SEPOLIA, + fallback: defaultInfuraKey + }) + case ChainId.Holesky: + return loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_INFURA_KEY_HOLESKY, + fallback: defaultInfuraKey + }) + + // L2 Mainnet + case ChainId.ArbitrumOne: + return loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_INFURA_KEY_ARBITRUM_ONE, + fallback: defaultInfuraKey + }) + case ChainId.Base: + return loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_INFURA_KEY_BASE, + fallback: defaultInfuraKey + }) + + // L2 Testnet + case ChainId.ArbitrumSepolia: + return loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_INFURA_KEY_ARBITRUM_SEPOLIA, + fallback: defaultInfuraKey + }) + case ChainId.BaseSepolia: + return loadEnvironmentVariableWithFallback({ + env: process.env.NEXT_PUBLIC_INFURA_KEY_BASE_SEPOLIA, + fallback: defaultInfuraKey + }) + } +} + +export function getInfuraRpcUrl( + chainId: InfuraSupportedChainId, + infuraKey: string = getInfuraKeyFromEnv(chainId) +): string { + switch (chainId) { + // L1 Mainnet + case ChainId.Ethereum: + return `https://mainnet.infura.io/v3/${infuraKey}` + + // L1 Testnet + case ChainId.Sepolia: + return `https://sepolia.infura.io/v3/${infuraKey}` + case ChainId.Holesky: + return `https://holesky.infura.io/v3/${infuraKey}` + + // L2 Mainnet + case ChainId.ArbitrumOne: + return `https://arbitrum-mainnet.infura.io/v3/${infuraKey}` + case ChainId.Base: + return `https://base-mainnet.infura.io/v3/${infuraKey}` + + // L2 Testnet + case ChainId.ArbitrumSepolia: + return `https://arbitrum-sepolia.infura.io/v3/${infuraKey}` + case ChainId.BaseSepolia: + return `https://base-sepolia.infura.io/v3/${infuraKey}` + } +} From 3dceb7a35398e730c915799d7ce82d265ab21f0e Mon Sep 17 00:00:00 2001 From: Dewansh Date: Thu, 16 Jan 2025 21:38:22 +0530 Subject: [PATCH 05/14] feat: check destination address for SC wallets (#2105) --- ...omDestinationAddressConfirmationDialog.tsx | 85 +++++++++++++++++++ .../TransferPanel/TransferPanel.tsx | 54 ++++++++++-- .../src/token-bridge-sdk/EthDepositStarter.ts | 19 ++--- 3 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/components/TransferPanel/CustomDestinationAddressConfirmationDialog.tsx diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomDestinationAddressConfirmationDialog.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomDestinationAddressConfirmationDialog.tsx new file mode 100644 index 0000000000..fef0231977 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/CustomDestinationAddressConfirmationDialog.tsx @@ -0,0 +1,85 @@ +import { useState } from 'react' + +import { Dialog, UseDialogProps } from '../common/Dialog' +import { ExternalLink } from '../common/ExternalLink' +import { useNetworks } from '../../hooks/useNetworks' +import { getExplorerUrl, getNetworkName } from '../../util/networks' +import { shortenAddress } from '../../util/CommonUtils' +import { useArbQueryParams } from '../../hooks/useArbQueryParams' +import { Checkbox } from '../common/Checkbox' + +export function CustomDestinationAddressConfirmationDialog( + props: UseDialogProps +) { + const [{ destinationAddress = '' }] = useArbQueryParams() + + const [networks] = useNetworks() + + const networkName = getNetworkName(networks.destinationChain.id) + + const [checkboxChecked, setCheckboxChecked] = useState(false) + + function closeWithReset(confirmed: boolean) { + props.onClose(confirmed) + setCheckboxChecked(false) + } + + return ( + +
+

+ You are attempting to deposit funds to the same address{' '} + + {shortenAddress(destinationAddress)} + {' '} + on {networkName}. +

+

+ This is an uncommon action since it's not guaranteed that you + have a smart contract wallet at the same address on the destination + chain. +

+
+ +

+ + I confirm that I have full control over the entered destination + address on {networkName} and understand that proceeding without + control may result in an{' '} + irrecoverable loss of funds. + + } + checked={checkboxChecked} + onChange={setCheckboxChecked} + /> +

+ +

+ If not sure, please reach out to us on our{' '} + + support channel + {' '} + for assistance. +

+
+ ) +} diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx index 5710d4ba57..0b55b4cd81 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanel.tsx @@ -19,6 +19,7 @@ import { useArbQueryParams } from '../../hooks/useArbQueryParams' import { useDialog } from '../common/Dialog' import { TokenApprovalDialog } from './TokenApprovalDialog' import { WithdrawalConfirmationDialog } from './WithdrawalConfirmationDialog' +import { CustomDestinationAddressConfirmationDialog } from './CustomDestinationAddressConfirmationDialog' import { TransferPanelSummary } from './TransferPanelSummary' import { useAppContextActions } from '../App/AppContext' import { trackEvent } from '../../util/AnalyticsUtils' @@ -173,6 +174,11 @@ export function TransferPanel() { openUSDCDepositConfirmationDialog ] = useDialog() + const [ + customDestinationAddressConfirmationDialogProps, + openCustomDestinationAddressConfirmationDialog + ] = useDialog() + const isCustomDestinationTransfer = !!latestDestinationAddress.current const { @@ -218,6 +224,13 @@ export function TransferPanel() { return isDepositMode && isUnbridgedToken }, [isDepositMode, selectedToken]) + const areSenderAndCustomDestinationAddressesEqual = useMemo(() => { + return ( + destinationAddress?.trim().toLowerCase() === + walletAddress?.trim().toLowerCase() + ) + }, [destinationAddress, walletAddress]) + async function depositToken() { if (!selectedToken) { throw new Error('Invalid app state: no selected token') @@ -335,6 +348,12 @@ export function TransferPanel() { setShowSmartContractWalletTooltip(true) }, 3000) + const confirmCustomDestinationAddressForSCWallets = async () => { + const waitForInput = openCustomDestinationAddressConfirmationDialog() + const [confirmed] = await waitForInput() + return confirmed + } + const transferCctp = async () => { if (!selectedToken) { return @@ -371,6 +390,16 @@ export function TransferPanel() { if (!withdrawalConfirmation) return } + // confirm if the user is certain about the custom destination address, especially if it matches the connected SCW address. + // this ensures that user funds do not end up in the destination chain’s address that matches their source-chain wallet address, which they may not control. + if ( + isSmartContractWallet && + areSenderAndCustomDestinationAddressesEqual + ) { + const confirmation = await confirmCustomDestinationAddressForSCWallets() + if (!confirmation) return false + } + const cctpTransferStarter = new CctpTransferStarter({ sourceChainProvider, destinationChainProvider @@ -560,12 +589,11 @@ export function TransferPanel() { destinationChainErc20Address }) - const { isNativeCurrencyTransfer, isWithdrawal } = - getBridgeTransferProperties({ - sourceChainId, - sourceChainErc20Address, - destinationChainId - }) + const { isWithdrawal } = getBridgeTransferProperties({ + sourceChainId, + sourceChainErc20Address, + destinationChainId + }) if (isWithdrawal && selectedToken && !sourceChainErc20Address) { /* @@ -588,6 +616,16 @@ export function TransferPanel() { const destinationAddress = latestDestinationAddress.current + // confirm if the user is certain about the custom destination address, especially if it matches the connected SCW address. + // this ensures that user funds do not end up in the destination chain’s address that matches their source-chain wallet address, which they may not control. + if ( + isSmartContractWallet && + areSenderAndCustomDestinationAddressesEqual + ) { + const confirmation = await confirmCustomDestinationAddressForSCWallets() + if (!confirmation) return false + } + const isCustomNativeTokenAmount2 = nativeCurrency.isCustom && isBatchTransferSupported && @@ -971,6 +1009,10 @@ export function TransferPanel() { amount={amount} /> + +
Date: Fri, 17 Jan 2025 19:03:11 +0800 Subject: [PATCH 06/14] feat: enable SC wallet to change destination chain (#2139) --- .../common/NetworkSelectionContainer.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx b/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx index aab6e65c24..ab0e0fc9b1 100644 --- a/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx @@ -123,7 +123,10 @@ export function NetworkButton({ const hasOneOrLessChain = chains.length <= 1 - const disabled = hasOneOrLessChain || isSmartContractWallet || isLoading + const disabled = + hasOneOrLessChain || + (isSmartContractWallet && type === 'source') || + isLoading const buttonStyle = { backgroundColor: getBridgeUiConfigForChain(selectedChainId).color @@ -405,6 +408,7 @@ export const NetworkSelectionContainer = ( const [oneNovaTransferDialogProps, openOneNovaTransferDialog] = useDialog() const [, setQueryParams] = useArbQueryParams() const { setAdvancedSettingsCollapsed } = useAdvancedSettingsStore() + const { isSmartContractWallet } = useAccountType() const isSource = props.type === 'source' @@ -442,7 +446,10 @@ export const NetworkSelectionContainer = ( actions.app.setSelectedToken(null) setQueryParams({ destinationAddress: undefined }) - setAdvancedSettingsCollapsed(true) + + if (!isSmartContractWallet) { + setAdvancedSettingsCollapsed(true) + } }, [ isSource, @@ -451,7 +458,8 @@ export const NetworkSelectionContainer = ( actions.app, setQueryParams, setAdvancedSettingsCollapsed, - openOneNovaTransferDialog + openOneNovaTransferDialog, + isSmartContractWallet ] ) From 6b65a9304fae0771fa92bd39f81a3a21de1950aa Mon Sep 17 00:00:00 2001 From: Bartek Date: Mon, 20 Jan 2025 13:20:52 +0100 Subject: [PATCH 07/14] test: enable URL query params e2e (#2196) --- .../tests/e2e/specfiles.json | 5 + .../tests/e2e/specs/urlQueryParam.cy.ts | 118 ++++++++---------- .../tests/support/common.ts | 1 + 3 files changed, 60 insertions(+), 64 deletions(-) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specfiles.json b/packages/arb-token-bridge-ui/tests/e2e/specfiles.json index fd907011e2..471031c4c8 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specfiles.json +++ b/packages/arb-token-bridge-ui/tests/e2e/specfiles.json @@ -58,5 +58,10 @@ "name": "Switch network", "file": "tests/e2e/specs/**/switchNetworks.cy.{js,jsx,ts,tsx}", "recordVideo": "false" + }, + { + "name": "URL query param", + "file": "tests/e2e/specs/**/urlQueryParam.cy.{js,jsx,ts,tsx}", + "recordVideo": "false" } ] diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts index 7aff7357bc..b4804bbea2 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts @@ -3,7 +3,12 @@ */ import { formatAmount } from '../../../src/util/NumberUtils' -import { getInitialETHBalance, visitAfterSomeDelay } from '../../support/common' +import { + getInitialETHBalance, + getL1NetworkName, + getL2NetworkName, + visitAfterSomeDelay +} from '../../support/common' describe('User enters site with query params on URL', () => { let l1ETHbal: number @@ -26,8 +31,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: 'max', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -38,26 +43,15 @@ describe('User enters site with query params on URL', () => { // it's very hard to get the max amount separately // so this test only asserts the amount set for the input field is less than user's balance // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` - cy.waitUntil( - () => - cy - .findAmountInput() - .invoke('val') - .should($val => { - cy.wrap(Number($val)).should('be.gt', 0) - }), - // optional timeouts and error messages - { - errorMsg: 'was expecting a numerical input value greater than 0', - timeout: 5000, - interval: 500 - } - ) - cy.findAmountInput() - .invoke('val') - .should($val => { - cy.wrap(Number($val)).should('be.lt', l1ETHbal) - }) + + cy.findAmountInput().should($el => { + const amount = parseFloat(String($el.val())) + expect(amount).to.be.gt(0) + }) + cy.findAmountInput().should($el => { + const amount = parseFloat(String($el.val())) + expect(amount).to.be.lt(Number(l1ETHbal)) + }) } ) context( @@ -66,8 +60,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: 'MAX', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -79,7 +73,7 @@ describe('User enters site with query params on URL', () => { // so this test only asserts the amount set for the input field is less than user's balance // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` cy.waitUntil( - () => cy.findAmountInput().then($el => Number($el.val()) > 0), + () => cy.findAmountInput().then($el => Number(String($el.val())) > 0), // optional timeouts and error messages { errorMsg: 'was expecting a numerical input value greater than 0', @@ -87,11 +81,10 @@ describe('User enters site with query params on URL', () => { interval: 500 } ) - cy.findAmountInput() - .invoke('val') - .should($val => { - cy.wrap(Number($val)).should('be.lt', l1ETHbal) - }) + cy.findAmountInput().should($el => { + const amount = parseFloat(String($el.val())) + expect(amount).to.be.lt(Number(l1ETHbal)) + }) } ) context( @@ -100,8 +93,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: 'MaX', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -115,12 +108,10 @@ describe('User enters site with query params on URL', () => { // but not the exact MAX AMOUNT set by the `setMaxAmount` function in `TransferPanelMain.tsx` cy.waitUntil( () => - cy - .findAmountInput() - .invoke('val') - .should($val => { - cy.wrap(Number($val)).should('be.gt', 0) - }), + cy.findAmountInput().should($el => { + const amount = parseFloat(String($el.val())) + expect(amount).to.be.gt(0) + }), // optional timeouts and error messages { errorMsg: 'was expecting a numerical input value greater than 0', @@ -128,19 +119,18 @@ describe('User enters site with query params on URL', () => { interval: 500 } ) - cy.findAmountInput() - .invoke('val') - .should($val => { - cy.wrap(Number($val)).should('be.lt', l1ETHbal) - }) + cy.findAmountInput().should($el => { + const amount = parseFloat(String($el.val())) + expect(amount).to.be.lt(Number(l1ETHbal)) + }) } ) context('?amount=56 should set transfer panel amount to 56', () => { visitAfterSomeDelay('/', { qs: { amount: '56', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -150,8 +140,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '1.6678', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -161,8 +151,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '6', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -172,8 +162,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '0.123', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -184,8 +174,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '-0.123', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -195,8 +185,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: 'asdfs', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -206,8 +196,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '0', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -217,8 +207,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '0.0001', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -228,8 +218,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '123,3,43', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) @@ -241,8 +231,8 @@ describe('User enters site with query params on URL', () => { visitAfterSomeDelay('/', { qs: { amount: '0, 123.222, 0.3', - sourceChain: 'custom-localhost', - destinationChain: 'arbitrum-localhost' + sourceChain: getL1NetworkName(), + destinationChain: getL2NetworkName() } }) diff --git a/packages/arb-token-bridge-ui/tests/support/common.ts b/packages/arb-token-bridge-ui/tests/support/common.ts index 807dda72b9..f278bbace2 100644 --- a/packages/arb-token-bridge-ui/tests/support/common.ts +++ b/packages/arb-token-bridge-ui/tests/support/common.ts @@ -188,6 +188,7 @@ export const visitAfterSomeDelay = ( ) => { cy.wait(15_000) // let all the race conditions settle, let UI load well first cy.visit(url, options) + cy.wait(15_000) } export const wait = (ms = 0): Promise => { From 0443f1bf14e34c5cdcaa74c55d067851d07d9341 Mon Sep 17 00:00:00 2001 From: Bartek Date: Mon, 20 Jan 2025 15:25:10 +0100 Subject: [PATCH 08/14] feat: network box redesign (#1893) Co-authored-by: Fionna Chan <13184582+fionnachan@users.noreply.github.com> Co-authored-by: Dewansh --- .../components/TransferPanel/TokenButton.tsx | 44 ++- .../components/TransferPanel/TokenInfo.tsx | 10 +- .../TransferPanel/TransferPanelMain.tsx | 38 +-- .../DestinationNetworkBox.tsx | 270 +++++++++++------- .../TransferPanelMain/SourceNetworkBox.tsx | 26 +- .../src/components/common/SafeImage.tsx | 13 +- .../tests/support/commands.ts | 2 + 7 files changed, 233 insertions(+), 170 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx index eaa43c8007..196a01c634 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenButton.tsx @@ -15,9 +15,12 @@ import { import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' import { Transition } from '../common/Transition' +import { SafeImage } from '../common/SafeImage' +import { useTokensFromLists, useTokensFromUser } from './TokenSearchUtils' export type TokenButtonOptions = { symbol?: string + logoSrc?: string disabled?: boolean } @@ -34,6 +37,9 @@ export function TokenButton({ const [networks] = useNetworks() const { childChainProvider } = useNetworksRelationship(networks) + const tokensFromLists = useTokensFromLists() + const tokensFromUser = useTokensFromUser() + const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) const tokenSymbol = useMemo(() => { @@ -51,6 +57,27 @@ export function TokenButton({ }) }, [selectedToken, networks.sourceChain.id, nativeCurrency.symbol, options]) + const tokenLogoSrc = useMemo(() => { + if (typeof options?.logoSrc !== 'undefined') { + return options.logoSrc || nativeCurrency.logoUrl + } + + if (selectedToken) { + return ( + tokensFromLists[selectedToken.address]?.logoURI ?? + tokensFromUser[selectedToken.address]?.logoURI + ) + } + + return nativeCurrency.logoUrl + }, [ + nativeCurrency.logoUrl, + options, + selectedToken, + tokensFromLists, + tokensFromUser + ]) + return ( <> @@ -63,18 +90,11 @@ export function TokenButton({ disabled={disabled} >
- {/* Commenting it out until we update the token image source files to be of better quality */} - {/* {tokenLogo && ( - // SafeImage is used for token logo, we don't know at buildtime - where those images will be loaded from // It would throw error - if it's loaded from external domains // eslint-disable-next-line - @next/next/no-img-element - Token logo - )} */} + {tokenSymbol} {!disabled && ( +
?
) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx index 463768f240..26017b9b19 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx @@ -3,7 +3,6 @@ import { ArrowsUpDownIcon, ArrowDownIcon } from '@heroicons/react/24/outline' import { twMerge } from 'tailwind-merge' import { utils } from 'ethers' import { Chain, useAccount } from 'wagmi' -import { useMedia } from 'react-use' import { useAppState } from '../../state' import { getExplorerUrl } from '../../util/networks' @@ -138,7 +137,6 @@ function CustomAddressBanner({ export function NetworkContainer({ network, customAddress, - bgLogoHeight = 58, children }: { network: Chain @@ -147,13 +145,7 @@ export function NetworkContainer({ children: React.ReactNode }) { const { address } = useAccount() - const { - color, - network: { logo: networkLogo } - } = getBridgeUiConfigForChain(network.id) - const isSmallScreen = useMedia('(max-width: 639px)') - - const backgroundImage = `url(${networkLogo})` + const { color } = getBridgeUiConfigForChain(network.id) const walletAddressLowercased = address?.toLowerCase() @@ -182,13 +174,7 @@ export function NetworkContainer({ showCustomAddressBanner && 'rounded-t-none' )} > -
+
{children}
@@ -197,26 +183,6 @@ export function NetworkContainer({ ) } -export function BalancesContainer({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
- ) -} - -export function NetworkListboxPlusBalancesContainer({ - children -}: { - children: React.ReactNode -}) { - return ( -
- {children} -
- ) -} - export function TransferPanelMain() { const [networks] = useNetworks() const { childChainProvider, isTeleportMode } = diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx index 87cf770bef..4d2417e1fb 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx @@ -1,16 +1,11 @@ +import { useMemo } from 'react' import { constants } from 'ethers' +import Image from 'next/image' import { useNetworks } from '../../../hooks/useNetworks' -import { - BalancesContainer, - NetworkContainer, - NetworkListboxPlusBalancesContainer -} from '../TransferPanelMain' -import { TokenBalance } from './TokenBalance' +import { NetworkContainer } from '../TransferPanelMain' import { useNetworksRelationship } from '../../../hooks/useNetworksRelationship' -import { NetworkType } from './utils' import { useAppState } from '../../../state' -import { sanitizeTokenSymbol } from '../../../util/TokenUtils' import { useBalances } from '../../../hooks/useBalances' import { CommonAddress } from '../../../util/CommonAddressUtils' import { isNetwork } from '../../../util/networks' @@ -24,139 +19,189 @@ import { } from '../../common/NetworkSelectionContainer' import { useNativeCurrencyBalances } from './useNativeCurrencyBalances' import { useIsBatchTransferSupported } from '../../../hooks/TransferPanel/useIsBatchTransferSupported' -import { ether } from '../../../constants' +import { getBridgeUiConfigForChain } from '../../../util/bridgeUiConfig' +import { SafeImage } from '../../common/SafeImage' +import { useTokensFromLists, useTokensFromUser } from '../TokenSearchUtils' import { formatAmount } from '../../../util/NumberUtils' import { Loader } from '../../common/atoms/Loader' import { useAmount2InputVisibility } from './SourceNetworkBox' -import { useIsCctpTransfer } from '../hooks/useIsCctpTransfer' import { useArbQueryParams } from '../../../hooks/useArbQueryParams' +import { useIsCctpTransfer } from '../hooks/useIsCctpTransfer' +import { sanitizeTokenSymbol } from '../../../util/TokenUtils' -function NativeCurrencyDestinationBalance({ prefix }: { prefix?: string }) { - const nativeCurrencyBalances = useNativeCurrencyBalances() +function BalanceRow({ + parentErc20Address, + balance, + symbolOverride +}: { + parentErc20Address?: string + balance: string | undefined + symbolOverride?: string +}) { const [networks] = useNetworks() - const nativeCurrency = useNativeCurrency({ - provider: networks.destinationChainProvider - }) - const { isDepositMode } = useNetworksRelationship(networks) - - if (nativeCurrency.isCustom) { - return ( - - ) - } - if (!nativeCurrencyBalances.destinationBalance) { - return ( -

- {prefix} - -

- ) - } + const { childChainProvider, isDepositMode } = + useNetworksRelationship(networks) + const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) + + const tokensFromLists = useTokensFromLists() + const tokensFromUser = useTokensFromUser() + + const tokenLogoSrc = useMemo(() => { + if (parentErc20Address) { + return ( + tokensFromLists[parentErc20Address]?.logoURI ?? + tokensFromUser[parentErc20Address]?.logoURI + ) + } + + return nativeCurrency.logoUrl + }, [ + nativeCurrency.logoUrl, + parentErc20Address, + tokensFromLists, + tokensFromUser + ]) + + const symbol = useMemo(() => { + if (symbolOverride) { + return symbolOverride + } + + if (parentErc20Address) { + return ( + tokensFromLists[parentErc20Address]?.symbol ?? + tokensFromUser[parentErc20Address]?.symbol + ) + } + + return nativeCurrency.symbol + }, [ + symbolOverride, + nativeCurrency.symbol, + parentErc20Address, + tokensFromLists, + tokensFromUser + ]) return ( -

- {prefix} - - {formatAmount(nativeCurrencyBalances.destinationBalance, { - symbol: ether.symbol - })} - -

+
+
+ + {symbol} +
+
+ Balance: + + {balance ? ( + balance + ) : ( + + )} + +
+
) } -function DestinationNetworkBalance() { +function BalancesContainer() { const { app: { selectedToken } } = useAppState() const [networks] = useNetworks() const { childChain, childChainProvider, isDepositMode } = useNetworksRelationship(networks) + const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) const { isArbitrumOne } = isNetwork(childChain.id) + const isCctpTransfer = useIsCctpTransfer() + + const isBatchTransferSupported = useIsBatchTransferSupported() + const { isAmount2InputVisible } = useAmount2InputVisibility() const { erc20ChildBalances } = useBalances() const nativeCurrencyBalances = useNativeCurrencyBalances() const selectedTokenBalances = useSelectedTokenBalances() - const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) + const selectedTokenDestinationBalance = isDepositMode + ? selectedTokenBalances.childBalance + : selectedTokenBalances.parentBalance - const isCctpTransfer = useIsCctpTransfer() + const selectedTokenOrNativeCurrencyBalance = selectedToken + ? selectedTokenDestinationBalance + : nativeCurrencyBalances.destinationBalance - if (selectedToken) { - return ( - <> - + {isCctpTransfer && isDepositMode && ( + + )} + + {isBatchTransferSupported && isAmount2InputVisible && ( + - {/* In deposit mode, when user selected USDC on mainnet, - the UI shows the Arb One balance of both USDC.e and native USDC */} - {isCctpTransfer && isDepositMode && ( - - )} - - ) - } - - if (nativeCurrency.isCustom) { - return ( - - ) - } - - return + )} +
+ ) } export function DestinationNetworkBox() { const [networks] = useNetworks() const [{ destinationAddress }] = useArbQueryParams() - const isBatchTransferSupported = useIsBatchTransferSupported() const [ destinationNetworkSelectionDialogProps, openDestinationNetworkSelectionDialog ] = useDialog() - const { isAmount2InputVisible } = useAmount2InputVisibility() + const { + network: { logo: networkLogo } + } = getBridgeUiConfigForChain(networks.destinationChain.id) return ( <> @@ -164,18 +209,21 @@ export function DestinationNetworkBox() { network={networks.destinationChain} customAddress={destinationAddress} > - +
- - - {isBatchTransferSupported && isAmount2InputVisible && ( - - )} - - +
+ {`${networks.destinationChain.name} +
+
+ - - + +
+ +
+ {`${networks.sourceChain.name} +
+
(false) useEffect(() => { + let mounted = true const image = new Image() if (typeof src === 'undefined') { @@ -18,13 +19,17 @@ export function SafeImage(props: SafeImageProps) { } else { const sanitizedImageSrc = sanitizeImageSrc(src) - image.onerror = () => setValidImageSrc(false) - image.onload = () => setValidImageSrc(sanitizedImageSrc) + image.onerror = () => { + if (mounted) setValidImageSrc(false) + } + image.onload = () => { + if (mounted) setValidImageSrc(sanitizedImageSrc) + } image.src = sanitizedImageSrc } - return function cleanup() { - // Abort previous loading + return () => { + mounted = false image.src = '' } }, [src]) diff --git a/packages/arb-token-bridge-ui/tests/support/commands.ts b/packages/arb-token-bridge-ui/tests/support/commands.ts index 64f4367fbe..b33df35b1c 100644 --- a/packages/arb-token-bridge-ui/tests/support/commands.ts +++ b/packages/arb-token-bridge-ui/tests/support/commands.ts @@ -141,10 +141,12 @@ export const fillCustomDestinationAddress = () => { // unlock custom destination address input cy.findByLabelText('Custom destination input lock') + .scrollIntoView() .should('be.visible') .click() cy.findByLabelText('Custom Destination Address Input') + .scrollIntoView() .should('be.visible') .type(Cypress.env('CUSTOM_DESTINATION_ADDRESS')) } From d83a4b09be113d68a8380c7194cef40fd8161c05 Mon Sep 17 00:00:00 2001 From: Bartek Date: Mon, 20 Jan 2025 16:54:28 +0100 Subject: [PATCH 09/14] fix: show native currency logo for amount2 (#2197) --- .../TransferPanel/TransferPanelMain/SourceNetworkBox.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx index 9c0a6c31f6..6346ab2dca 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/SourceNetworkBox.tsx @@ -160,11 +160,13 @@ export function SourceNetworkBox() { nativeCurrencyDecimalsOnSourceChain ) ) - : undefined + : undefined, + logoSrc: nativeCurrency.logoUrl }), [ - nativeCurrencyBalances, nativeCurrency.symbol, + nativeCurrency.logoUrl, + nativeCurrencyBalances.sourceBalance, nativeCurrencyDecimalsOnSourceChain ] ) From 7e4cb2a560c66f70310d139cf873df5161704864 Mon Sep 17 00:00:00 2001 From: Fionna Chan <13184582+fionnachan@users.noreply.github.com> Date: Tue, 21 Jan 2025 01:23:49 +0800 Subject: [PATCH 10/14] refactor: use source dest in useSelectedTokenBalances (#2177) --- .../DestinationNetworkBox.tsx | 6 +- .../TransferPanelMain/useMaxAmount.ts | 8 +- .../TransferPanel/TransferPanelMainInput.tsx | 14 +- .../TransferPanel/useSelectedTokenBalances.ts | 59 ++--- .../useSelectedTokenBalances.test.ts | 202 ++++++++++++++++++ 5 files changed, 241 insertions(+), 48 deletions(-) create mode 100644 packages/arb-token-bridge-ui/src/hooks/__tests__/useSelectedTokenBalances.test.ts diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx index 4d2417e1fb..88c5134900 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/DestinationNetworkBox.tsx @@ -129,12 +129,8 @@ function BalancesContainer() { const nativeCurrencyBalances = useNativeCurrencyBalances() const selectedTokenBalances = useSelectedTokenBalances() - const selectedTokenDestinationBalance = isDepositMode - ? selectedTokenBalances.childBalance - : selectedTokenBalances.parentBalance - const selectedTokenOrNativeCurrencyBalance = selectedToken - ? selectedTokenDestinationBalance + ? selectedTokenBalances.destinationBalance : nativeCurrencyBalances.destinationBalance return ( diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts index b307de1f0a..330bd0de64 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain/useMaxAmount.ts @@ -80,9 +80,7 @@ export function useMaxAmount() { const maxAmount = useMemo(() => { if (selectedToken) { - const tokenBalance = isDepositMode - ? selectedTokenBalances.parentBalance - : selectedTokenBalances.childBalance + const tokenBalance = selectedTokenBalances.sourceBalance if (!tokenBalance) { return undefined @@ -98,10 +96,8 @@ export function useMaxAmount() { return nativeCurrencyMaxAmount }, [ selectedToken, - isDepositMode, nativeCurrencyMaxAmount, - selectedTokenBalances.parentBalance, - selectedTokenBalances.childBalance + selectedTokenBalances.sourceBalance ]) const maxAmount2 = useMemo(() => { diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx index e6365719d6..3757f1ffed 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx @@ -30,8 +30,6 @@ function MaxButton({ const { app: { selectedToken } } = useAppState() - const [networks] = useNetworks() - const { isDepositMode } = useNetworksRelationship(networks) const selectedTokenBalances = useSelectedTokenBalances() const nativeCurrencyBalances = useNativeCurrencyBalances() @@ -39,9 +37,7 @@ function MaxButton({ const maxButtonVisible = useMemo(() => { const nativeCurrencySourceBalance = nativeCurrencyBalances.sourceBalance - const tokenBalance = isDepositMode - ? selectedTokenBalances.parentBalance - : selectedTokenBalances.childBalance + const tokenBalance = selectedTokenBalances.sourceBalance if (selectedToken) { return tokenBalance && !tokenBalance.isZero() @@ -50,9 +46,7 @@ function MaxButton({ return nativeCurrencySourceBalance && !nativeCurrencySourceBalance.isZero() }, [ nativeCurrencyBalances.sourceBalance, - isDepositMode, - selectedTokenBalances.parentBalance, - selectedTokenBalances.childBalance, + selectedTokenBalances.sourceBalance, selectedToken ]) @@ -94,9 +88,7 @@ function SourceChainTokenBalance({ const nativeCurrency = useNativeCurrency({ provider: childChainProvider }) - const tokenBalance = isDepositMode - ? selectedTokenBalances.parentBalance - : selectedTokenBalances.childBalance + const tokenBalance = selectedTokenBalances.sourceBalance const balance = balanceOverride ?? diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts index 2083f5868e..5d17483f7a 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useSelectedTokenBalances.ts @@ -9,16 +9,18 @@ import { import { CommonAddress } from '../../util/CommonAddressUtils' import { isNetwork } from '../../util/networks' import { useBalances } from '../useBalances' +import { useNetworksRelationship } from '../useNetworksRelationship' export type Balances = { - parentBalance: BigNumber | null - childBalance: BigNumber | null + sourceBalance: BigNumber | null + destinationBalance: BigNumber | null } export function useSelectedTokenBalances(): Balances { const { app } = useAppState() const { selectedToken } = app const [networks] = useNetworks() + const { isDepositMode } = useNetworksRelationship(networks) const { isArbitrumOne: isSourceChainArbitrumOne, @@ -45,16 +47,19 @@ export function useSelectedTokenBalances(): Balances { return useMemo(() => { const result: Balances = { - parentBalance: null, - childBalance: null + sourceBalance: null, + destinationBalance: null } if (!selectedToken) { return result } + let parentBalance: BigNumber | null = null + let childBalance: BigNumber | null = null + if (erc20ParentBalances) { - result.parentBalance = + parentBalance = erc20ParentBalances[selectedToken.address.toLowerCase()] ?? null } @@ -63,50 +68,52 @@ export function useSelectedTokenBalances(): Balances { selectedToken.l2Address && selectedToken.l2Address in erc20ChildBalances ) { - result.childBalance = + childBalance = erc20ChildBalances[selectedToken.l2Address.toLowerCase()] ?? constants.Zero } // token not bridged to the child chain, show zero if (!selectedToken.l2Address) { - result.childBalance = constants.Zero + childBalance = constants.Zero } if ( isTokenArbitrumOneNativeUSDC(selectedToken.address) && - isEthereumArbitrumOnePair && - erc20ParentBalances && - erc20ChildBalances + isEthereumArbitrumOnePair ) { - return { - parentBalance: - erc20ParentBalances[CommonAddress.Ethereum.USDC.toLowerCase()] ?? - null, - childBalance: - erc20ChildBalances[selectedToken.address.toLowerCase()] ?? null - } + parentBalance = + erc20ParentBalances?.[CommonAddress.Ethereum.USDC.toLowerCase()] ?? null + childBalance = + erc20ChildBalances?.[selectedToken.address.toLowerCase()] ?? null } if ( isTokenArbitrumSepoliaNativeUSDC(selectedToken.address.toLowerCase()) && - isSepoliaArbSepoliaPair && - erc20ParentBalances && - erc20ChildBalances + isSepoliaArbSepoliaPair ) { + parentBalance = + erc20ParentBalances?.[CommonAddress.Sepolia.USDC.toLowerCase()] ?? null + childBalance = + erc20ChildBalances?.[selectedToken.address.toLowerCase()] ?? null + } + + if (isDepositMode) { return { - parentBalance: - erc20ParentBalances[CommonAddress.Sepolia.USDC.toLowerCase()] ?? null, - childBalance: - erc20ChildBalances[selectedToken.address.toLowerCase()] ?? null + sourceBalance: parentBalance, + destinationBalance: childBalance } } - return result + return { + sourceBalance: childBalance, + destinationBalance: parentBalance + } }, [ erc20ParentBalances, erc20ChildBalances, isEthereumArbitrumOnePair, isSepoliaArbSepoliaPair, - selectedToken + selectedToken, + isDepositMode ]) } diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useSelectedTokenBalances.test.ts b/packages/arb-token-bridge-ui/src/hooks/__tests__/useSelectedTokenBalances.test.ts new file mode 100644 index 0000000000..9e93745464 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useSelectedTokenBalances.test.ts @@ -0,0 +1,202 @@ +import { renderHook } from '@testing-library/react' +import { BigNumber, constants } from 'ethers' + +import { getProviderForChainId } from '@/token-bridge-sdk/utils' +import { getWagmiChain } from '../../util/wagmi/getWagmiChain' +import { useNetworks } from '../useNetworks' +import { useBalances } from '../useBalances' +import { useSelectedTokenBalances } from '../TransferPanel/useSelectedTokenBalances' +import { useAppState } from '../../state' +import { ChainId } from '../../types/ChainId' + +jest.mock('../useNetworks', () => ({ + useNetworks: jest.fn() +})) + +jest.mock('../useBalances', () => ({ + useBalances: jest.fn() +})) + +jest.mock('../../state', () => ({ + useAppState: jest.fn().mockReturnValue({ + app: { + selectedToken: { + type: 'ERC20', + decimals: 18, + name: 'random', + symbol: 'RAND', + address: '0x123', + l2Address: '0x234', + listIds: new Set('1') + } + } + }) +})) + +describe('useSelectedTokenBalances', () => { + const mockedUseNetworks = jest.mocked(useNetworks) + const mockedUseBalances = jest.mocked(useBalances) + const mockedUseAppState = jest.mocked(useAppState) + + beforeAll(() => { + mockedUseBalances.mockReturnValue({ + ethParentBalance: BigNumber.from(100_000), + erc20ParentBalances: { + '0x123': BigNumber.from(200_000), + '0x222': BigNumber.from(250_000_000) + }, + ethChildBalance: BigNumber.from(300_000), + erc20ChildBalances: { '0x234': BigNumber.from(400_000) }, + updateEthChildBalance: jest.fn(), + updateEthParentBalance: jest.fn(), + updateErc20ParentBalances: jest.fn(), + updateErc20ChildBalances: jest.fn() + }) + }) + + it('should return ERC20 parent balance as source balance and ERC20 child balance as destination balance when source chain is Sepolia and destination chain is Arbitrum Sepolia, and selected token address on Sepolia is 0x123', () => { + mockedUseNetworks.mockReturnValue([ + { + sourceChain: getWagmiChain(ChainId.Sepolia), + sourceChainProvider: getProviderForChainId(ChainId.Sepolia), + destinationChain: getWagmiChain(ChainId.ArbitrumSepolia), + destinationChainProvider: getProviderForChainId(ChainId.ArbitrumSepolia) + }, + jest.fn() + ]) + + const { result } = renderHook(useSelectedTokenBalances) + expect(result.current).toEqual({ + sourceBalance: BigNumber.from(200_000), + destinationBalance: BigNumber.from(400_000) + }) + }) + + it('should return ERC20 child balance as source balance and ERC20 parent balance as destination balance when source chain is Arbitrum Sepolia and destination chain is Sepolia, and selected token address on Sepolia is 0x123', () => { + mockedUseNetworks.mockReturnValue([ + { + sourceChain: getWagmiChain(ChainId.ArbitrumSepolia), + sourceChainProvider: getProviderForChainId(ChainId.ArbitrumSepolia), + destinationChain: getWagmiChain(ChainId.Sepolia), + destinationChainProvider: getProviderForChainId(ChainId.Sepolia) + }, + jest.fn() + ]) + + const { result } = renderHook(useSelectedTokenBalances) + expect(result.current).toEqual({ + sourceBalance: BigNumber.from(400_000), + destinationBalance: BigNumber.from(200_000) + }) + }) + + it('should return ERC20 parent balance as source balance and zero as destination balance when source chain is Sepolia and destination chain is Arbitrum Sepolia, and selected token address on Sepolia is 0x222 but without child chain address (unbridged token)', () => { + mockedUseAppState.mockReturnValueOnce({ + app: { + selectedToken: { + type: 'ERC20', + decimals: 18, + name: 'random', + symbol: 'RAND', + address: '0x222', + listIds: new Set('2') + } + } + }) + + mockedUseNetworks.mockReturnValue([ + { + sourceChain: getWagmiChain(ChainId.Sepolia), + sourceChainProvider: getProviderForChainId(ChainId.Sepolia), + destinationChain: getWagmiChain(ChainId.ArbitrumSepolia), + destinationChainProvider: getProviderForChainId(ChainId.ArbitrumSepolia) + }, + jest.fn() + ]) + + const { result } = renderHook(useSelectedTokenBalances) + expect(result.current).toEqual({ + sourceBalance: BigNumber.from(250_000_000), + destinationBalance: constants.Zero + }) + }) + + it('should return zero as source balance and ERC20 parent balance as destination balance when source chain is Arbitrum Sepolia and destination chain is Sepolia, and selected token address on Sepolia is 0x222 but without child chain address (unbridged token)', () => { + mockedUseAppState.mockReturnValueOnce({ + app: { + selectedToken: { + type: 'ERC20', + decimals: 18, + name: 'random', + symbol: 'RAND', + address: '0x222', + listIds: new Set('2') + } + } + }) + + mockedUseNetworks.mockReturnValue([ + { + sourceChain: getWagmiChain(ChainId.ArbitrumSepolia), + sourceChainProvider: getProviderForChainId(ChainId.ArbitrumSepolia), + destinationChain: getWagmiChain(ChainId.Sepolia), + destinationChainProvider: getProviderForChainId(ChainId.Sepolia) + }, + jest.fn() + ]) + + const { result } = renderHook(useSelectedTokenBalances) + expect(result.current).toEqual({ + sourceBalance: constants.Zero, + destinationBalance: BigNumber.from(250_000_000) + }) + }) + + it('should return null as source balance and null as destination balance when source chain is Sepolia and destination chain is Arbitrum Sepolia, and selected token is null', () => { + mockedUseAppState.mockReturnValueOnce({ + app: { + selectedToken: null + } + }) + + mockedUseNetworks.mockReturnValue([ + { + sourceChain: getWagmiChain(ChainId.Sepolia), + sourceChainProvider: getProviderForChainId(ChainId.Sepolia), + destinationChain: getWagmiChain(ChainId.ArbitrumSepolia), + destinationChainProvider: getProviderForChainId(ChainId.ArbitrumSepolia) + }, + jest.fn() + ]) + + const { result } = renderHook(useSelectedTokenBalances) + expect(result.current).toEqual({ + sourceBalance: null, + destinationBalance: null + }) + }) + + it('should return null as source balance and null as destination balance when source chain is Arbitrum Sepolia and destination chain is Sepolia, and selected token is null', () => { + mockedUseAppState.mockReturnValueOnce({ + app: { + selectedToken: null + } + }) + + mockedUseNetworks.mockReturnValue([ + { + sourceChain: getWagmiChain(ChainId.ArbitrumSepolia), + sourceChainProvider: getProviderForChainId(ChainId.ArbitrumSepolia), + destinationChain: getWagmiChain(ChainId.Sepolia), + destinationChainProvider: getProviderForChainId(ChainId.Sepolia) + }, + jest.fn() + ]) + + const { result } = renderHook(useSelectedTokenBalances) + expect(result.current).toEqual({ + sourceBalance: null, + destinationBalance: null + }) + }) +}) From ec728908a562c35d11d367c4c24a61ae3c5c3e61 Mon Sep 17 00:00:00 2001 From: Fionna Chan <13184582+fionnachan@users.noreply.github.com> Date: Wed, 22 Jan 2025 00:09:06 +0800 Subject: [PATCH 11/14] refactor: use useSWR in useUpdateUsdcBalances (#2164) --- .../TransactionHistorySearchBar.tsx | 2 +- .../components/TransferPanel/TokenSearch.tsx | 6 +- .../TransferPanel/TransferPanelMain.tsx | 15 ++- .../hooks/useDestinationAddressError.ts | 2 +- .../src/components/common/AddCustomChain.tsx | 2 +- .../components/syncers/useBalanceUpdater.tsx | 12 +- .../src/hooks/CCTP/useUpdateUSDCBalances.ts | 88 ------------ .../src/hooks/CCTP/useUpdateUsdcBalances.ts | 127 ++++++++++++++++++ .../__tests__/useUpdateUsdcBalances.test.ts | 79 +++++++++++ 9 files changed, 230 insertions(+), 103 deletions(-) delete mode 100644 packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUSDCBalances.ts create mode 100644 packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts create mode 100644 packages/arb-token-bridge-ui/src/hooks/__tests__/useUpdateUsdcBalances.test.ts diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx index f6486fcc35..2a0bdb165d 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionHistorySearchBar.tsx @@ -1,5 +1,5 @@ import { create } from 'zustand' -import { isAddress } from 'ethers/lib/utils.js' +import { isAddress } from 'ethers/lib/utils' import { Address, useAccount } from 'wagmi' import { useCallback, useEffect } from 'react' import { MagnifyingGlassIcon } from '@heroicons/react/24/outline' diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx index 8d90a95caa..bae5436094 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TokenSearch.tsx @@ -29,7 +29,7 @@ import { warningToast } from '../common/atoms/Toast' import { CommonAddress } from '../../util/CommonAddressUtils' import { ArbOneNativeUSDC } from '../../util/L2NativeUtils' import { getNetworkName, isNetwork } from '../../util/networks' -import { useUpdateUSDCBalances } from '../../hooks/CCTP/useUpdateUSDCBalances' +import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' import { useAccountType } from '../../hooks/useAccountType' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { SearchPanelTable } from '../common/SearchPanel/SearchPanelTable' @@ -538,7 +538,7 @@ export function TokenSearch({ parentChainProvider, isTeleportMode } = useNetworksRelationship(networks) - const { updateUSDCBalances } = useUpdateUSDCBalances({ walletAddress }) + const { updateUsdcBalances } = useUpdateUsdcBalances({ walletAddress }) const { isLoading: isLoadingAccountType } = useAccountType() const { openDialog: openTransferDisabledDialog } = useTransferDisabledDialogStore() @@ -574,7 +574,7 @@ export function TokenSearch({ return } - await updateUSDCBalances() + updateUsdcBalances() // if an Orbit chain is selected we need to fetch its USDC address let childChainUsdcAddress diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx index 26017b9b19..a42e5533c1 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMain.tsx @@ -3,6 +3,7 @@ import { ArrowsUpDownIcon, ArrowDownIcon } from '@heroicons/react/24/outline' import { twMerge } from 'tailwind-merge' import { utils } from 'ethers' import { Chain, useAccount } from 'wagmi' +import { isAddress } from 'ethers/lib/utils' import { useAppState } from '../../state' import { getExplorerUrl } from '../../util/networks' @@ -15,7 +16,7 @@ import { isTokenSepoliaUSDC, isTokenMainnetUSDC } from '../../util/TokenUtils' -import { useUpdateUSDCBalances } from '../../hooks/CCTP/useUpdateUSDCBalances' +import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' import { useNativeCurrency } from '../../hooks/useNativeCurrency' import { useNetworks } from '../../hooks/useNetworks' import { useNetworksRelationship } from '../../hooks/useNetworksRelationship' @@ -202,8 +203,12 @@ export function TransferPanelMain() { const { updateErc20ParentBalances, updateErc20ChildBalances } = useBalances() - const { updateUSDCBalances } = useUpdateUSDCBalances({ - walletAddress: destinationAddressOrWalletAddress + const { updateUsdcBalances } = useUpdateUsdcBalances({ + walletAddress: + destinationAddressOrWalletAddress && + isAddress(destinationAddressOrWalletAddress) + ? destinationAddressOrWalletAddress + : undefined }) useEffect(() => { @@ -228,7 +233,7 @@ export function TransferPanelMain() { isTokenArbitrumOneNativeUSDC(selectedToken.address) || isTokenArbitrumSepoliaNativeUSDC(selectedToken.address)) ) { - updateUSDCBalances() + updateUsdcBalances() return } @@ -241,7 +246,7 @@ export function TransferPanelMain() { updateErc20ParentBalances, updateErc20ChildBalances, destinationAddressOrWalletAddress, - updateUSDCBalances, + updateUsdcBalances, isTeleportMode ]) diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useDestinationAddressError.ts b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useDestinationAddressError.ts index 890a735445..5d41f1bc50 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useDestinationAddressError.ts +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/hooks/useDestinationAddressError.ts @@ -1,6 +1,6 @@ import { useMemo } from 'react' import useSWRImmutable from 'swr/immutable' -import { isAddress } from 'ethers/lib/utils.js' +import { isAddress } from 'ethers/lib/utils' import { DestinationAddressErrors } from '../AdvancedSettings' import { addressIsDenylisted } from '../../../util/AddressUtils' diff --git a/packages/arb-token-bridge-ui/src/components/common/AddCustomChain.tsx b/packages/arb-token-bridge-ui/src/components/common/AddCustomChain.tsx index 9be3138a8d..f63ec399b3 100644 --- a/packages/arb-token-bridge-ui/src/components/common/AddCustomChain.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/AddCustomChain.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { isAddress } from 'ethers/lib/utils.js' +import { isAddress } from 'ethers/lib/utils' import { Popover } from '@headlessui/react' import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' import { StaticJsonRpcProvider } from '@ethersproject/providers' diff --git a/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx b/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx index 728322ced2..7b20f2cfa7 100644 --- a/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx +++ b/packages/arb-token-bridge-ui/src/components/syncers/useBalanceUpdater.tsx @@ -2,7 +2,8 @@ import { useInterval, useLatest } from 'react-use' import { useAccount } from 'wagmi' import { useAppState } from '../../state' -import { useUpdateUSDCBalances } from '../../hooks/CCTP/useUpdateUSDCBalances' +import { useUpdateUsdcBalances } from '../../hooks/CCTP/useUpdateUsdcBalances' +import { isTokenNativeUSDC } from '../../util/TokenUtils' // Updates all balances periodically export function useBalanceUpdater() { @@ -12,14 +13,17 @@ export function useBalanceUpdater() { const { address: walletAddress } = useAccount() const latestTokenBridge = useLatest(arbTokenBridge) - const { updateUSDCBalances } = useUpdateUSDCBalances({ + const { updateUsdcBalances } = useUpdateUsdcBalances({ walletAddress }) useInterval(() => { - updateUSDCBalances() - if (selectedToken) { + if (isTokenNativeUSDC(selectedToken.address)) { + updateUsdcBalances() + return + } + latestTokenBridge?.current?.token?.updateTokenData(selectedToken.address) } }, 10000) diff --git a/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUSDCBalances.ts b/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUSDCBalances.ts deleted file mode 100644 index 48e1bca0db..0000000000 --- a/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUSDCBalances.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { useCallback } from 'react' -import { isAddress } from 'ethers/lib/utils.js' - -import { CommonAddress } from '../../util/CommonAddressUtils' -import { getL2ERC20Address } from '../../util/TokenUtils' -import { useNetworks } from '../useNetworks' -import { useNetworksRelationship } from '../useNetworksRelationship' -import { isNetwork } from '../../util/networks' -import { useBalances } from '../useBalances' -import { Address } from 'wagmi' - -export function useUpdateUSDCBalances({ - walletAddress -}: { - walletAddress: string | undefined -}) { - const [networks] = useNetworks() - const { parentChainProvider, parentChain, childChainProvider } = - useNetworksRelationship(networks) - - const _walletAddress: Address | undefined = - walletAddress && isAddress(walletAddress) ? walletAddress : undefined - const { - updateErc20ParentBalances: updateErc20ParentBalance, - updateErc20ChildBalances: updateErc20ChildBalance - } = useBalances({ - parentWalletAddress: _walletAddress, - childWalletAddress: _walletAddress - }) - - const updateUSDCBalances = useCallback(async () => { - const { isEthereumMainnet, isSepolia, isArbitrumOne, isArbitrumSepolia } = - isNetwork(parentChain.id) - - let parentChainUsdcAddress, childChainUsdcAddress: string | undefined - - if (isEthereumMainnet || isSepolia) { - parentChainUsdcAddress = isEthereumMainnet - ? CommonAddress.Ethereum.USDC - : CommonAddress.Sepolia.USDC - - childChainUsdcAddress = isEthereumMainnet - ? CommonAddress.ArbitrumOne.USDC - : CommonAddress.ArbitrumSepolia.USDC - } - - if (isArbitrumOne || isArbitrumSepolia) { - parentChainUsdcAddress = isArbitrumOne - ? CommonAddress.ArbitrumOne.USDC - : CommonAddress.ArbitrumSepolia.USDC - } - - // USDC is not native for the selected networks, do nothing - if (!parentChainUsdcAddress) { - return - } - - updateErc20ParentBalance([parentChainUsdcAddress]) - - // we don't have native USDC addresses for Orbit chains, we need to fetch it - if (!childChainUsdcAddress) { - try { - childChainUsdcAddress = ( - await getL2ERC20Address({ - erc20L1Address: parentChainUsdcAddress, - l1Provider: parentChainProvider, - l2Provider: childChainProvider - }) - ).toLowerCase() - } catch { - // could be never bridged before - return - } - } - - if (childChainUsdcAddress) { - updateErc20ChildBalance([childChainUsdcAddress]) - } - }, [ - childChainProvider, - parentChain.id, - parentChainProvider, - updateErc20ParentBalance, - updateErc20ChildBalance - ]) - - return { updateUSDCBalances } -} diff --git a/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts b/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts new file mode 100644 index 0000000000..dde96b1a69 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/CCTP/useUpdateUsdcBalances.ts @@ -0,0 +1,127 @@ +import { useCallback } from 'react' +import { Address } from 'wagmi' +import useSWRImmutable from 'swr/immutable' + +import { CommonAddress } from '../../util/CommonAddressUtils' +import { getL2ERC20Address } from '../../util/TokenUtils' +import { useNetworks } from '../useNetworks' +import { useNetworksRelationship } from '../useNetworksRelationship' +import { isNetwork } from '../../util/networks' +import { useBalances } from '../useBalances' +import { getProviderForChainId } from '@/token-bridge-sdk/utils' + +export async function getChildUsdcAddress({ + parentChainId, + childChainId +}: { + parentChainId: number + childChainId: number +}) { + const { + isEthereumMainnet: isParentEthereumMainnet, + isSepolia: isParentSepolia + } = isNetwork(parentChainId) + + if (isParentEthereumMainnet) { + return CommonAddress.ArbitrumOne.USDC + } + + if (isParentSepolia) { + return CommonAddress.ArbitrumSepolia.USDC + } + + const parentUsdcAddress = getParentUsdcAddress(parentChainId) + const parentProvider = getProviderForChainId(parentChainId) + const childProvider = getProviderForChainId(childChainId) + + if (!parentUsdcAddress) { + return + } + + return getL2ERC20Address({ + erc20L1Address: parentUsdcAddress, + l1Provider: parentProvider, + l2Provider: childProvider + }) +} + +export function getParentUsdcAddress(parentChainId: number) { + const { + isEthereumMainnet: isParentEthereumMainnet, + isSepolia: isParentSepolia, + isArbitrumOne: isParentArbitrumOne, + isArbitrumSepolia: isParentArbitrumSepolia + } = isNetwork(parentChainId) + + if (isParentEthereumMainnet) { + return CommonAddress.Ethereum.USDC + } + + if (isParentSepolia) { + return CommonAddress.Sepolia.USDC + } + + if (isParentArbitrumOne) { + return CommonAddress.ArbitrumOne.USDC + } + + if (isParentArbitrumSepolia) { + return CommonAddress.ArbitrumSepolia.USDC + } +} + +export function useUpdateUsdcBalances({ + walletAddress +}: { + walletAddress: Address | undefined +}) { + const [networks] = useNetworks() + const { parentChain, childChain } = useNetworksRelationship(networks) + + const { + updateErc20ParentBalances: updateErc20ParentBalance, + updateErc20ChildBalances: updateErc20ChildBalance + } = useBalances({ + parentWalletAddress: walletAddress, + childWalletAddress: walletAddress + }) + + // we don't have native USDC addresses for Orbit chains, we need to fetch it + const { data: childUsdcAddress, isLoading } = useSWRImmutable( + [parentChain.id, childChain.id, 'getChildUsdcAddress'], + ([parentChainId, childChainId]) => + getChildUsdcAddress({ + parentChainId, + childChainId + }) + ) + + const updateUsdcBalances = useCallback(() => { + const parentUsdcAddress = getParentUsdcAddress(parentChain.id) + + // USDC is not native for the selected networks, do nothing + if (!parentUsdcAddress) { + return + } + + if (isLoading) { + return + } + + updateErc20ParentBalance([parentUsdcAddress.toLowerCase()]) + + if (childUsdcAddress) { + updateErc20ChildBalance([childUsdcAddress.toLowerCase()]) + } + }, [ + isLoading, + childUsdcAddress, + parentChain.id, + updateErc20ChildBalance, + updateErc20ParentBalance + ]) + + return { + updateUsdcBalances + } +} diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useUpdateUsdcBalances.test.ts b/packages/arb-token-bridge-ui/src/hooks/__tests__/useUpdateUsdcBalances.test.ts new file mode 100644 index 0000000000..ac3358d499 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useUpdateUsdcBalances.test.ts @@ -0,0 +1,79 @@ +import { CommonAddress } from '../../util/CommonAddressUtils' +import { + getChildUsdcAddress, + getParentUsdcAddress +} from '../CCTP/useUpdateUsdcBalances' +import { getL2ERC20Address } from '../../util/TokenUtils' +import { ChainId } from '../../types/ChainId' + +jest.mock('../../util/TokenUtils', () => ({ + getL2ERC20Address: jest.fn() +})) + +const xaiTestnetChainId = 37714555429 as ChainId + +describe('getParentUsdcAddress', () => { + it('should return native USDC address on Ethereum when parent chain is Ethereum (1)', () => { + const result = getParentUsdcAddress(ChainId.Ethereum) + expect(result).toEqual(CommonAddress.Ethereum.USDC) + }) + + it('should return native USDC address on Sepolia when parent chain is Sepolia (11155111)', () => { + const result = getParentUsdcAddress(ChainId.Sepolia) + expect(result).toEqual(CommonAddress.Sepolia.USDC) + }) + + it('should return native USDC address on Arbitrum One when parent chain is Arbitrum One (42161)', () => { + const result = getParentUsdcAddress(ChainId.ArbitrumOne) + expect(result).toEqual(CommonAddress.ArbitrumOne.USDC) + }) + + it('should return native USDC address on Arbitrum Sepolia when parent chain is Arbitrum Sepolia (421614)', () => { + const result = getParentUsdcAddress(ChainId.ArbitrumSepolia) + expect(result).toEqual(CommonAddress.ArbitrumSepolia.USDC) + }) + + it('should return undefined when parent chain is Base (8453)', () => { + const result = getParentUsdcAddress(ChainId.Base) + expect(result).toEqual(undefined) + }) + + it('should return undefined when parent chain is Base Sepolia (84532)', () => { + const result = getParentUsdcAddress(ChainId.BaseSepolia) + expect(result).toEqual(undefined) + }) +}) + +describe('getChildUsdcAddress', () => { + it('should return native USDC address on Arbitrum One when parent USDC address is native USDC on Ethereum, parent chain is Ethereum, and child chain is Arbitrum One', async () => { + const result = await getChildUsdcAddress({ + parentChainId: ChainId.Ethereum, + childChainId: ChainId.ArbitrumOne + }) + + expect(result).toEqual(CommonAddress.ArbitrumOne.USDC) + }) + + it('should return native USDC address on Arbitrum Sepolia when parent USDC address is native USDC on Sepolia, parent chain is Sepolia, and child chain is Arbitrum Sepolia', async () => { + const result = await getChildUsdcAddress({ + parentChainId: ChainId.Sepolia, + childChainId: ChainId.ArbitrumSepolia + }) + + expect(result).toEqual(CommonAddress.ArbitrumSepolia.USDC) + }) + + it('should return USDC address on Xai Testnet when parent USDC address is native USDC on Arbitrum Sepolia, parent chain is Arbitrum Sepolia, and child chain is Xai Testnet', async () => { + const mockedGetL2ERC20Address = jest + .mocked(getL2ERC20Address) + .mockResolvedValueOnce('0xBd8C9bFBB225bFF89C7884060338150dAA626Edb') + + const result = await getChildUsdcAddress({ + parentChainId: ChainId.ArbitrumSepolia, + childChainId: xaiTestnetChainId + }) + + expect(result).toEqual('0xBd8C9bFBB225bFF89C7884060338150dAA626Edb') + expect(mockedGetL2ERC20Address).toHaveBeenCalledTimes(1) + }) +}) From 930bbe1fbf4e7f2a9632c9f4d24f710f343adfcd Mon Sep 17 00:00:00 2001 From: Dewansh Date: Wed, 22 Jan 2025 20:36:24 +0530 Subject: [PATCH 12/14] perf: cache transaction receipts (#2181) --- .../src/token-bridge-sdk/EnhancedProvider.ts | 163 ++++++++++++++++++ .../__tests__/EnhancedProvider.test.ts | 132 ++++++++++++++ .../src/token-bridge-sdk/utils.ts | 4 +- 3 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 packages/arb-token-bridge-ui/src/token-bridge-sdk/EnhancedProvider.ts create mode 100644 packages/arb-token-bridge-ui/src/token-bridge-sdk/__tests__/EnhancedProvider.test.ts diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/EnhancedProvider.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/EnhancedProvider.ts new file mode 100644 index 0000000000..c579509dce --- /dev/null +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/EnhancedProvider.ts @@ -0,0 +1,163 @@ +import { + Networkish, + StaticJsonRpcProvider, + TransactionReceipt +} from '@ethersproject/providers' +import { BigNumber } from 'ethers' +import { ChainId } from '../types/ChainId' +import { ConnectionInfo } from 'ethers/lib/utils.js' +import { isNetwork } from '../util/networks' + +interface Storage { + getItem(key: string): string | null + setItem(key: string, value: string): void +} + +class WebStorage implements Storage { + getItem(key: string): string | null { + return localStorage.getItem(key) + } + + setItem(key: string, value: string): void { + localStorage.setItem(key, value) + } +} + +const localStorageKey = `arbitrum:bridge:tx-receipts-cache` +const enableCaching = true + +const getCacheKey = (chainId: number | string, txHash: string) => + `${chainId}:${txHash}`.toLowerCase() + +/** + * Converts a TransactionReceipt to a string by encoding BigNumber properties. + * @param txReceipt - The receipt to encode. + * @returns The encoded receipt as a string. + */ +const txReceiptToString = (txReceipt: TransactionReceipt): string => { + const encodedReceipt: Record = { ...txReceipt } + + Object.keys(encodedReceipt).forEach(field => { + if (encodedReceipt[field]?._isBigNumber) { + encodedReceipt[field] = { + _type: 'BigNumber', + _data: encodedReceipt[field].toString() + } + } + }) + + return JSON.stringify(encodedReceipt) +} + +/** + * Converts a stringified receipt back to a TransactionReceipt by decoding BigNumber properties. + * @param stringified - The stringified receipt to decode. + * @returns The decoded TransactionReceipt. + */ +const txReceiptFromString = (stringified: string): TransactionReceipt => { + const receipt = JSON.parse(stringified) + const decodedReceipt = { ...receipt } + + Object.keys(decodedReceipt).forEach(field => { + if (decodedReceipt[field]?._type === 'BigNumber') { + decodedReceipt[field] = BigNumber.from(decodedReceipt[field]._data) + } + }) + + return decodedReceipt as TransactionReceipt +} + +/** + * Checks if a transaction receipt can be cached based on its chain and confirmations. + * @param chainId - The ID of the chain. + * @param txReceipt - The transaction receipt to check. + * @returns True if the receipt can be cached, false otherwise. + */ +export const shouldCacheTxReceipt = ( + chainId: number, + txReceipt: TransactionReceipt +): boolean => { + if (!enableCaching) return false + + // for now, only enable caching for testnets, + if (!isNetwork(chainId).isTestnet) { + return false + } + + // Finality checks to avoid caching re-org'ed transactions + if ( + (chainId === ChainId.Ethereum && txReceipt.confirmations < 65) || + (chainId === ChainId.Sepolia && txReceipt.confirmations < 5) || + (chainId === ChainId.Holesky && txReceipt.confirmations < 5) + ) { + return false + } + + return true +} + +function getTxReceiptFromCache( + storage: Storage, + chainId: number, + txHash: string +) { + if (!enableCaching) return undefined + + const cachedReceipts = JSON.parse(storage.getItem(localStorageKey) || '{}') + const receipt = cachedReceipts[getCacheKey(chainId, txHash)] + + if (!receipt) return undefined + + return txReceiptFromString(JSON.stringify(receipt)) +} + +function addTxReceiptToCache( + storage: Storage, + chainId: number, + txReceipt: TransactionReceipt +) { + const cachedReceipts = JSON.parse(storage.getItem(localStorageKey) || '{}') + + const key = getCacheKey(chainId, txReceipt.transactionHash) + storage.setItem( + localStorageKey, + JSON.stringify({ + ...cachedReceipts, + [key]: JSON.parse(txReceiptToString(txReceipt)) + }) + ) +} + +export class EnhancedProvider extends StaticJsonRpcProvider { + private storage: Storage + + constructor( + url?: ConnectionInfo | string, + network?: Networkish, + storage: Storage = new WebStorage() + ) { + super(url, network) + this.storage = storage + } + + async getTransactionReceipt( + transactionHash: string | Promise + ): Promise { + const hash = await transactionHash + const chainId = this.network.chainId + + // Retrieve the cached receipt for the hash, if it exists + const cachedReceipt = getTxReceiptFromCache(this.storage, chainId, hash) + if (cachedReceipt) return cachedReceipt + + // Else, fetch the receipt using the original method + const receipt = await super.getTransactionReceipt(hash) + + // Cache the receipt if it meets the criteria + if (receipt && shouldCacheTxReceipt(chainId, receipt)) { + addTxReceiptToCache(this.storage, chainId, receipt) + } + + return receipt + } +} diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/__tests__/EnhancedProvider.test.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/__tests__/EnhancedProvider.test.ts new file mode 100644 index 0000000000..da89af8753 --- /dev/null +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/__tests__/EnhancedProvider.test.ts @@ -0,0 +1,132 @@ +import { + StaticJsonRpcProvider, + TransactionReceipt +} from '@ethersproject/providers' +import { BigNumber } from 'ethers' +import { ChainId } from '../../types/ChainId' +import { rpcURLs } from '../../util/networks' +import { EnhancedProvider, shouldCacheTxReceipt } from '../EnhancedProvider' + +class TestStorage { + private store: Record = {} + getItem(key: string): string | null { + return this.store[key] || null + } + setItem(key: string, value: string): void { + this.store[key] = value + } + clear(): void { + this.store = {} + } +} + +// a type-safe test transaction receipt which can be extended as per our test cases +const testTxReceipt: TransactionReceipt = { + to: '0x99E2d366BA678522F3793d2c2E758Ac29a59678E', + from: '0x5Be7Babe02224e944582493613b00A4Caf4d56df', + contractAddress: '', + transactionIndex: 27, + gasUsed: BigNumber.from(1000000000), + logsBloom: '', + blockHash: '', + transactionHash: + '0x40c993848fc927ced60283b2188734b50e736d305d462289cbe231876c6570a8', + logs: [], + blockNumber: 7483636, + confirmations: 19088, + cumulativeGasUsed: BigNumber.from(1000000000), + effectiveGasPrice: BigNumber.from(1000000000), + status: 1, + type: 2, + byzantium: true +} + +describe('EnhancedProvider', () => { + let storage: TestStorage + + beforeEach(() => { + storage = new TestStorage() + jest.restoreAllMocks() + }) + + it('should fetch real transaction and use cache for subsequent requests', async () => { + const rpcUrl = rpcURLs[ChainId.Sepolia]! + + const provider = new EnhancedProvider(rpcUrl, ChainId.Sepolia, storage) + // Wait for network to be initialized + await provider.ready + + const txHash = + '0x019ae29f37f27399fc2ac3f640b05e7e3700c75759026a4b4d51d7e572480e4c' + + // Mock super.getTransactionReceipt to return a successful receipt + const mockReceipt = { ...testTxReceipt, transactionHash: txHash } + + // Spy on the parent class's getTransactionReceipt that fires the RPC call + const superGetReceipt = jest + .spyOn(StaticJsonRpcProvider.prototype, 'getTransactionReceipt') + .mockResolvedValue(mockReceipt) + + // First request - should hit the network + const firstReceipt = await provider.getTransactionReceipt(txHash) + expect(firstReceipt).toBeTruthy() + expect(superGetReceipt).toHaveBeenCalledTimes(1) + + // Check if cache was populated + const cache = storage.getItem('arbitrum:bridge:tx-receipts-cache') + expect(cache).toBeTruthy() + + // Second request - should use cache + const secondReceipt = await provider.getTransactionReceipt(txHash) + expect(secondReceipt).toEqual(firstReceipt) + expect(superGetReceipt).toHaveBeenCalledTimes(1) // No additional RPC calls + }) + + describe('Caching condition tests', () => { + let provider: EnhancedProvider + + beforeEach(async () => { + provider = new EnhancedProvider( + 'https://mock.url', + ChainId.Sepolia, + storage + ) + // Mock the network property + Object.defineProperty(provider, 'network', { + value: { chainId: ChainId.Sepolia, name: 'sepolia' }, + writable: true + }) + // Wait for provider to be ready + await provider.ready + }) + + it('should not cache receipt when confirmations are below threshold', async () => { + const mockReceipt = { + ...testTxReceipt, + confirmations: 4 // Below Sepolia threshold of 5 + } + + expect(shouldCacheTxReceipt(ChainId.Sepolia, mockReceipt)).toBeFalsy() + }) + + it('should cache receipt when confirmations are above threshold', async () => { + const mockReceipt = { + ...testTxReceipt, + confirmations: 10 // Above Sepolia threshold of 5 + } + + // Mock the parent class's getTransactionReceipt + jest + .spyOn(StaticJsonRpcProvider.prototype, 'getTransactionReceipt') + .mockResolvedValue(mockReceipt) + + const receipt = await provider.getTransactionReceipt( + mockReceipt.transactionHash + ) + expect(receipt).toBeTruthy() + + const cache = storage.getItem('arbitrum:bridge:tx-receipts-cache') + expect(cache).toBeTruthy() + }) + }) +}) diff --git a/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts index b8bc6ae6dc..f402888397 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts @@ -13,6 +13,7 @@ import { getArbitrumNetwork } from '@arbitrum/sdk' import { isDepositMode } from '../util/isDepositMode' +import { EnhancedProvider } from './EnhancedProvider' export const getAddressFromSigner = async (signer: Signer) => { const address = await signer.getAddress() @@ -107,7 +108,8 @@ const getProviderForChainCache: { function createProviderWithCache(chainId: ChainId) { const rpcUrl = rpcURLs[chainId] - const provider = new StaticJsonRpcProvider(rpcUrl, chainId) + + const provider = new EnhancedProvider(rpcUrl, chainId) getProviderForChainCache[chainId] = provider return provider } From 4ef459f89dbb31b10f0d90b7b53d9d40801dcc1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:06:52 +0100 Subject: [PATCH 13/14] build(deps): bump undici from 5.28.4 to 5.28.5 (#2199) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 130 +++++++++++++++++++++++++++--------------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/yarn.lock b/yarn.lock index 23414ead7c..f63c007b24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1768,10 +1768,10 @@ "@motionone/dom" "^10.16.2" tslib "^2.3.1" -"@next/env@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.22.tgz#8898ae47595badbfacebfc1585f42a4e06a97301" - integrity sha512-EQ6y1QeNQglNmNIXvwP/Bb+lf7n9WtgcWvtoFsHquVLCJUuxRs+6SfZ5EK0/EqkkLex4RrDySvKgKNN7PXip7Q== +"@next/env@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.23.tgz#3003b53693cbc476710b856f83e623c8231a6be9" + integrity sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA== "@next/eslint-plugin-next@14.2.5": version "14.2.5" @@ -1787,50 +1787,50 @@ dependencies: glob "7.1.7" -"@next/swc-darwin-arm64@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.22.tgz#2b3fcb42247ba951b19a48fc03f1d6fe65629baa" - integrity sha512-HUaLiehovgnqY4TMBZJ3pDaOsTE1spIXeR10pWgdQVPYqDGQmHJBj3h3V6yC0uuo/RoY2GC0YBFRkOX3dI9WVQ== - -"@next/swc-darwin-x64@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.22.tgz#11ecc609e9530b3edf8ddfd1fd3bd6aca4e1bfda" - integrity sha512-ApVDANousaAGrosWvxoGdLT0uvLBUC+srqOcpXuyfglA40cP2LBFaGmBjhgpxYk5z4xmunzqQvcIgXawTzo2uQ== - -"@next/swc-linux-arm64-gnu@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.22.tgz#4c08dd223e50c348f561af2285e27fb326ffabbf" - integrity sha512-3O2J99Bk9aM+d4CGn9eEayJXHuH9QLx0BctvWyuUGtJ3/mH6lkfAPRI4FidmHMBQBB4UcvLMfNf8vF0NZT7iKw== - -"@next/swc-linux-arm64-musl@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.22.tgz#0b285336f145887d421b3762f3d7c75f847ec1b3" - integrity sha512-H/hqfRz75yy60y5Eg7DxYfbmHMjv60Dsa6IWHzpJSz4MRkZNy5eDnEW9wyts9bkxwbOVZNPHeb3NkqanP+nGPg== - -"@next/swc-linux-x64-gnu@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.22.tgz#a936b6cfea0364571102f0389c6368d6acf3e294" - integrity sha512-LckLwlCLcGR1hlI5eiJymR8zSHPsuruuwaZ3H2uudr25+Dpzo6cRFjp/3OR5UYJt8LSwlXv9mmY4oI2QynwpqQ== - -"@next/swc-linux-x64-musl@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.22.tgz#0359497840d0b7d8c095d0d9735bc6aec68cef5d" - integrity sha512-qGUutzmh0PoFU0fCSu0XYpOfT7ydBZgDfcETIeft46abPqP+dmePhwRGLhFKwZWxNWQCPprH26TjaTxM0Nv8mw== - -"@next/swc-win32-arm64-msvc@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.22.tgz#9fd249d49ffccf3388400ab24472c432cdd04c24" - integrity sha512-K6MwucMWmIvMb9GlvT0haYsfIPxfQD8yXqxwFy4uLFMeXIb2TcVYQimxkaFZv86I7sn1NOZnpOaVk5eaxThGIw== - -"@next/swc-win32-ia32-msvc@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.22.tgz#70d8d5a48e78c7382c3e0544af28c2788ca6b551" - integrity sha512-5IhDDTPEbzPR31ZzqHe90LnNe7BlJUZvC4sA1thPJV6oN5WmtWjZ0bOYfNsyZx00FJt7gggNs6SrsX0UEIcIpA== - -"@next/swc-win32-x64-msvc@14.2.22": - version "14.2.22" - resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.22.tgz#b034f544c1346093a235f6bba46497a1ba344fc1" - integrity sha512-nvRaB1PyG4scn9/qNzlkwEwLzuoPH3Gjp7Q/pLuwUgOTt1oPMlnCI3A3rgkt+eZnU71emOiEv/mR201HoURPGg== +"@next/swc-darwin-arm64@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz#6d83f03e35e163e8bbeaf5aeaa6bf55eed23d7a1" + integrity sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ== + +"@next/swc-darwin-x64@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz#e02abc35d5e36ce1550f674f8676999f293ba54f" + integrity sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q== + +"@next/swc-linux-arm64-gnu@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz#f13516ad2d665950951b59e7c239574bb8504d63" + integrity sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg== + +"@next/swc-linux-arm64-musl@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz#10d05a1c161dc8426d54ccf6d9bbed6953a3252a" + integrity sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g== + +"@next/swc-linux-x64-gnu@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz#7f5856df080f58ba058268b30429a2ab52500536" + integrity sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ== + +"@next/swc-linux-x64-musl@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz#d494ebdf26421c91be65f9b1d095df0191c956d8" + integrity sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA== + +"@next/swc-win32-arm64-msvc@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz#62786e7ba4822a20b6666e3e03e5a389b0e7eb3b" + integrity sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ== + +"@next/swc-win32-ia32-msvc@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz#ef028af91e1c40a4ebba0d2c47b23c1eeb299594" + integrity sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA== + +"@next/swc-win32-x64-msvc@14.2.23": + version "14.2.23" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz#c81838f02f2f16a321b7533890fb63c1edec68e1" + integrity sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw== "@noble/curves@1.2.0", "@noble/curves@~1.2.0": version "1.2.0" @@ -10811,12 +10811,12 @@ next-query-params@^5.0.0: dependencies: tslib "^2.0.3" -next@^14.2.21: - version "14.2.22" - resolved "https://registry.yarnpkg.com/next/-/next-14.2.22.tgz#0cd664916ef4c725f31fa812d870348cffd0115b" - integrity sha512-Ps2caobQ9hlEhscLPiPm3J3SYhfwfpMqzsoCMZGWxt9jBRK9hoBZj2A37i8joKhsyth2EuVKDVJCTF5/H4iEDw== +next@^14.2.22: + version "14.2.23" + resolved "https://registry.yarnpkg.com/next/-/next-14.2.23.tgz#37edc9a4d42c135fd97a4092f829e291e2e7c943" + integrity sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw== dependencies: - "@next/env" "14.2.22" + "@next/env" "14.2.23" "@swc/helpers" "0.5.5" busboy "1.6.0" caniuse-lite "^1.0.30001579" @@ -10824,15 +10824,15 @@ next@^14.2.21: postcss "8.4.31" styled-jsx "5.1.1" optionalDependencies: - "@next/swc-darwin-arm64" "14.2.22" - "@next/swc-darwin-x64" "14.2.22" - "@next/swc-linux-arm64-gnu" "14.2.22" - "@next/swc-linux-arm64-musl" "14.2.22" - "@next/swc-linux-x64-gnu" "14.2.22" - "@next/swc-linux-x64-musl" "14.2.22" - "@next/swc-win32-arm64-msvc" "14.2.22" - "@next/swc-win32-ia32-msvc" "14.2.22" - "@next/swc-win32-x64-msvc" "14.2.22" + "@next/swc-darwin-arm64" "14.2.23" + "@next/swc-darwin-x64" "14.2.23" + "@next/swc-linux-arm64-gnu" "14.2.23" + "@next/swc-linux-arm64-musl" "14.2.23" + "@next/swc-linux-x64-gnu" "14.2.23" + "@next/swc-linux-x64-musl" "14.2.23" + "@next/swc-win32-arm64-msvc" "14.2.23" + "@next/swc-win32-ia32-msvc" "14.2.23" + "@next/swc-win32-x64-msvc" "14.2.23" no-case@^3.0.4: version "3.0.4" @@ -14214,9 +14214,9 @@ undici-types@~6.19.2: integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== undici@^5.25.4: - version "5.28.4" - resolved "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz" - integrity sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g== + version "5.28.5" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.5.tgz#b2b94b6bf8f1d919bc5a6f31f2c01deb02e54d4b" + integrity sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA== dependencies: "@fastify/busboy" "^2.0.0" From 388202b8a91c8de8ae2225b86f10822420103d8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:18:10 +0100 Subject: [PATCH 14/14] build(deps-dev): bump vite from 5.4.7 to 5.4.12 (#2200) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/scripts/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 9e7591a891..5198540ea6 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -36,7 +36,7 @@ "dotenv": "^16.4.5", "eslint": "^8.0.0", "typescript": "^5.0.0", - "vite": "^5.4.7", + "vite": "^5.4.12", "vitest": "^0.34.0" } } diff --git a/yarn.lock b/yarn.lock index f63c007b24..2212170b88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14513,10 +14513,10 @@ vite-node@0.34.6: picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0 || ^5.0.0-0" -"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^5.4.7: - version "5.4.7" - resolved "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz" - integrity sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ== +"vite@^3.0.0 || ^4.0.0 || ^5.0.0-0", "vite@^3.1.0 || ^4.0.0 || ^5.0.0-0", vite@^5.4.12: + version "5.4.12" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.12.tgz#627d12ff06de3942557dfe8632fd712a12a072c7" + integrity sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA== dependencies: esbuild "^0.21.3" postcss "^8.4.43"