diff --git a/packages/bridge-ui-v2/src/components/Activities/Activities.svelte b/packages/bridge-ui-v2/src/components/Activities/Activities.svelte index 2096c12826..30bd8f937e 100644 --- a/packages/bridge-ui-v2/src/components/Activities/Activities.svelte +++ b/packages/bridge-ui-v2/src/components/Activities/Activities.svelte @@ -11,7 +11,6 @@ import { Spinner } from '$components/Spinner'; import { activitiesConfig } from '$config'; import { type BridgeTransaction, fetchTransactions } from '$libs/bridge'; - import { web3modal } from '$libs/connect'; import { bridgeTxService } from '$libs/storage'; import { account, network } from '$stores'; import type { Account } from '$stores/account'; @@ -51,8 +50,6 @@ return bridgeTx.slice(start, end); }; - const onWalletConnect = () => web3modal.openModal(); - const onAccountChange = async (newAccount: Account, oldAccount?: Account) => { // We want to make sure that we are connected and only // fetch if the account has changed diff --git a/packages/bridge-ui-v2/src/components/Activities/Transaction.svelte b/packages/bridge-ui-v2/src/components/Activities/Transaction.svelte index d9691a4ba7..271b416353 100644 --- a/packages/bridge-ui-v2/src/components/Activities/Transaction.svelte +++ b/packages/bridge-ui-v2/src/components/Activities/Transaction.svelte @@ -2,12 +2,12 @@ import { t } from 'svelte-i18n'; import { formatEther } from 'viem'; - import type { BridgeTransaction, MessageStatus } from '$libs/bridge'; + import type { BridgeTransaction } from '$libs/bridge'; import { chainUrlMap } from '$libs/chain'; export let item: BridgeTransaction; - import { createEventDispatcher, onDestroy } from 'svelte'; + import { createEventDispatcher } from 'svelte'; import { DesktopOrLarger } from '$components/DesktopOrLarger'; import { Icon } from '$components/Icon'; @@ -23,19 +23,6 @@ const handlePress = () => dispatch('press'); - const mapStatusToText = (status: MessageStatus) => { - switch (status) { - case 1: - return 'Pending'; - case 2: - return 'Claimed'; - case 3: - return 'Failed'; - default: - return 'Unknown'; - } - }; - let attrs = isDesktopOrLarger ? {} : { role: 'button' }; diff --git a/packages/bridge-ui-v2/src/components/Bridge/ProcessingFee/ProcessingFee.svelte b/packages/bridge-ui-v2/src/components/Bridge/ProcessingFee/ProcessingFee.svelte index 9a98bfd23e..de20dc7254 100644 --- a/packages/bridge-ui-v2/src/components/Bridge/ProcessingFee/ProcessingFee.svelte +++ b/packages/bridge-ui-v2/src/components/Bridge/ProcessingFee/ProcessingFee.svelte @@ -9,7 +9,6 @@ import { InputBox } from '$components/InputBox'; import { LoadingText } from '$components/LoadingText'; import { Tooltip } from '$components/Tooltip'; - import { processingFeeComponent } from '$config'; import { ProcessingFeeMethod } from '$libs/fee'; import { parseToWei } from '$libs/util/parseToWei'; import { uid } from '$libs/util/uid'; @@ -63,12 +62,6 @@ closeModal(); } - function closeModalWithDelay() { - // By adding delay there is enough time to see the selected option - // before closing the modal. Better experience for the user. - setTimeout(closeModal, processingFeeComponent.closingDelayOptionClick); - } - function focusInputBox() { inputBox.focus(); } diff --git a/packages/bridge-ui-v2/src/components/InputBox/InputBox.svelte b/packages/bridge-ui-v2/src/components/InputBox/InputBox.svelte index 8f54a2ac5c..9183c06a66 100644 --- a/packages/bridge-ui-v2/src/components/InputBox/InputBox.svelte +++ b/packages/bridge-ui-v2/src/components/InputBox/InputBox.svelte @@ -1,6 +1,4 @@ @@ -114,7 +125,7 @@ {#if isDesktopOrLarger} - + {:else} {/if} diff --git a/packages/bridge-ui-v2/src/libs/bridge/checkBalanceToBridge.ts b/packages/bridge-ui-v2/src/libs/bridge/checkBalanceToBridge.ts index 393a259fa9..7563464d2c 100644 --- a/packages/bridge-ui-v2/src/libs/bridge/checkBalanceToBridge.ts +++ b/packages/bridge-ui-v2/src/libs/bridge/checkBalanceToBridge.ts @@ -97,8 +97,8 @@ export async function checkBalanceToBridge({ const isTokenAlreadyDeployed = await isDeployedCrossChain({ token, - srcChainId: destChainId, - destChainId: srcChainId, + srcChainId, + destChainId, }); try { diff --git a/packages/bridge-ui-v2/src/libs/fee/recommendProcessingFee.ts b/packages/bridge-ui-v2/src/libs/fee/recommendProcessingFee.ts index b028386003..6036406beb 100644 --- a/packages/bridge-ui-v2/src/libs/fee/recommendProcessingFee.ts +++ b/packages/bridge-ui-v2/src/libs/fee/recommendProcessingFee.ts @@ -1,8 +1,10 @@ import { getPublicClient } from '@wagmi/core'; -import { zeroAddress } from 'viem'; import { recommentProcessingFee } from '$config'; -import { getAddress, type Token, TokenType } from '$libs/token'; +import { isDeployedCrossChain, type Token, TokenType } from '$libs/token'; +import { getLogger } from '$libs/util/logger'; + +const log = getLogger('libs:recommendedProcessingFee'); type RecommendProcessingFeeArgs = { token: Token; @@ -25,16 +27,22 @@ export async function recommendProcessingFee({ token, destChainId, srcChainId }: throw Error('missing required source chain for ERC20 token'); } - const tokenAddress = await getAddress({ token, srcChainId, destChainId }); + const isTokenAlreadyDeployed = await isDeployedCrossChain({ + token, + srcChainId, + destChainId, + }); - if (!tokenAddress || tokenAddress === zeroAddress) { - // Gas limit for erc20 if not deployed on the destination chain - // already is about ~2.9m, so we add some to make it enticing - gasLimit = erc20NotDeployedGasLimit; - } else { + if (isTokenAlreadyDeployed) { // Gas limit for erc20 if already deployed on the destination chain is // about ~1m, so again, add some to ensure processing gasLimit = erc20DeployedGasLimit; + log(`token ${token.symbol} is already deployed on chain ${destChainId}`); + } else { + // Gas limit for erc20 if not deployed on the destination chain + // already is about ~2.9m, so we add some to make it enticing + gasLimit = erc20NotDeployedGasLimit; + log(`token ${token.symbol} is not deployed on chain ${destChainId}`); } } diff --git a/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts b/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts index 91d50bcac5..d9e73adaa2 100644 --- a/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts +++ b/packages/bridge-ui-v2/src/libs/storage/CustomTokenService.ts @@ -64,7 +64,7 @@ export class CustomTokenService implements TokenService { // Filter out zero addresses from the new token's addresses const filteredAddresses = Object.fromEntries( - Object.entries(token.addresses).filter(([key, value]) => value !== zeroAddress), + Object.entries(token.addresses).filter(([, value]) => value !== zeroAddress), ); // Find the stored token to update diff --git a/packages/bridge-ui-v2/src/libs/token/getAddress.test.ts b/packages/bridge-ui-v2/src/libs/token/getAddress.test.ts index 6818da3c2f..bc11325cd8 100644 --- a/packages/bridge-ui-v2/src/libs/token/getAddress.test.ts +++ b/packages/bridge-ui-v2/src/libs/token/getAddress.test.ts @@ -1,8 +1,6 @@ -import { getContract, type GetContractResult } from '@wagmi/core'; +import { type Address, getContract, type GetContractResult } from '@wagmi/core'; -import { tokenVaultABI } from '$abi'; import { PUBLIC_L1_CHAIN_ID, PUBLIC_L2_CHAIN_ID } from '$env/static/public'; -import { chainContractsMap } from '$libs/chain'; import { getAddress } from './getAddress'; import { ETHToken, testERC20Tokens } from './tokens'; @@ -16,46 +14,33 @@ const HORSEToken = testERC20Tokens[1]; const mockTokenContract = { read: { canonicalToBridged: vi.fn(), + isBridgedToken: vi.fn(), + bridgedToCanonical: vi.fn(), }, } as unknown as GetContractResult; describe('getAddress', () => { - beforeAll(() => { - vi.mocked(getContract).mockReturnValue(mockTokenContract); - }); + beforeEach(() => { + vi.resetAllMocks(); - it('should return undefined if ETH', async () => { - expect(await getAddress({ token: ETHToken, srcChainId: Number(PUBLIC_L1_CHAIN_ID) })).toBeUndefined(); + vi.mocked(getContract).mockReturnValue(mockTokenContract); }); - it('should return the address if ERC20 and has address on the source chain', async () => { - expect(await getAddress({ token: HORSEToken, srcChainId: Number(PUBLIC_L1_CHAIN_ID) })).toEqual( - HORSEToken.addresses[PUBLIC_L1_CHAIN_ID], - ); + describe('ETH Tests', () => { + it('should return undefined if ETH', async () => { + expect(await getAddress({ token: ETHToken, srcChainId: Number(PUBLIC_L1_CHAIN_ID) })).toBeUndefined(); + }); }); - it('should return undefined if ERC20 and has no address on the source chain and no destination chain is is passed in', async () => { - expect(await getAddress({ token: HORSEToken, srcChainId: Number(PUBLIC_L2_CHAIN_ID) })).toBeUndefined(); - }); + describe('ERC20 Tests', () => { + it('should return the address if ERC20 and has address on the source chain', async () => { + expect(await getAddress({ token: HORSEToken, srcChainId: Number(PUBLIC_L1_CHAIN_ID) })).toEqual( + HORSEToken.addresses[PUBLIC_L1_CHAIN_ID], + ); + }); - it('should return the address of deployed ERC20 token', async () => { - vi.mocked(mockTokenContract.read.canonicalToBridged).mockResolvedValue('0x456789'); - - expect( - await getAddress({ - token: HORSEToken, - srcChainId: Number(PUBLIC_L2_CHAIN_ID), - destChainId: Number(PUBLIC_L1_CHAIN_ID), - }), - ).toEqual('0x456789'); - expect(mockTokenContract.read.canonicalToBridged).toHaveBeenCalledWith([ - BigInt(1), - HORSEToken.addresses[PUBLIC_L1_CHAIN_ID], - ]); - expect(getContract).toHaveBeenCalledWith({ - abi: tokenVaultABI, - address: chainContractsMap[PUBLIC_L2_CHAIN_ID].tokenVaultAddress, - chainId: Number(PUBLIC_L2_CHAIN_ID), + it('should return undefined if ERC20 and has no address on the source chain and no destination chain is is passed in', async () => { + expect(await getAddress({ token: HORSEToken, srcChainId: Number(PUBLIC_L2_CHAIN_ID) })).toBeUndefined(); }); }); }); diff --git a/packages/bridge-ui-v2/src/libs/token/getCrossChainAddress.test.ts b/packages/bridge-ui-v2/src/libs/token/getCrossChainAddress.test.ts index 254c759780..61c0df32d2 100644 --- a/packages/bridge-ui-v2/src/libs/token/getCrossChainAddress.test.ts +++ b/packages/bridge-ui-v2/src/libs/token/getCrossChainAddress.test.ts @@ -1,5 +1,160 @@ +import { getContract, type GetContractResult } from '@wagmi/core'; +import { zeroAddress } from 'viem'; + +import { getCrossChainAddress } from './getCrossChainAddress'; +import { type Token, TokenType } from './types'; + +vi.mock('@wagmi/core'); +vi.mock('$abi'); + +const MockedETH: Token = { + name: '', + addresses: {}, + symbol: '', + decimals: 0, + type: TokenType.ETH, +}; + +let mockToken: Token; + +const mockTokenVaultContract = { + read: { + canonicalToBridged: vi.fn(), + isBridgedToken: vi.fn(), + bridgedToCanonical: vi.fn(), + }, +} as unknown as GetContractResult; + +vi.mock('$libs/chain', () => ({ + chainContractsMap: { + 1: { + tokenVaultAddress: '0x00001', + }, + 2: { + tokenVaultAddress: '0x00002', + }, + }, +})); + describe('getCrossChainAddress', () => { - it('TODO', () => { - expect(true).toBeTruthy(); + const srcChainId = 1; + const destChainId = 2; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(getContract).mockReturnValue(mockTokenVaultContract); + mockToken = { + name: 'MockToken', + addresses: { + 1: '0x123456', + 2: '0x654321', + }, + symbol: 'MOCK', + decimals: 18, + type: TokenType.ERC20, + }; + }); + + it('should return null for ETH type tokens', async () => { + const result = await getCrossChainAddress({ + token: MockedETH, + srcChainId: 1, + destChainId: 2, + }); + expect(result).toBeNull(); + }); + + it('should return the bridged address of the token on the destination chain if not already stored', async () => { + // Given + vi.mocked(mockTokenVaultContract.read.bridgedToCanonical).mockResolvedValue([ + destChainId, + mockToken.addresses[destChainId], + ]); + vi.mocked(mockTokenVaultContract.read.canonicalToBridged).mockResolvedValue(mockToken.addresses[destChainId]); + + // temporarily store the mocked address so we do not have to hardcode variables + const preconfiguredAddress = mockToken.addresses[destChainId]; + + // set the destination address it to zero so we can test it + mockToken.addresses[destChainId] = zeroAddress; + + // When + const result = await getCrossChainAddress({ + token: mockToken, + srcChainId, + destChainId, + }); + + // Then + expect(result).toEqual(preconfiguredAddress); + expect(mockTokenVaultContract.read.bridgedToCanonical).toHaveBeenCalledWith([mockToken.addresses[srcChainId]]); + expect(mockTokenVaultContract.read.canonicalToBridged).toHaveBeenCalledWith([ + BigInt(destChainId), + preconfiguredAddress, + ]); + }); + + it('should return the bridged address if stored or configured', async () => { + // When + const result = await getCrossChainAddress({ + token: mockToken, + srcChainId, + destChainId, + }); + + // Then + expect(result).toEqual(mockToken.addresses[destChainId]); + expect(mockTokenVaultContract.read.bridgedToCanonical).not.toHaveBeenCalled(); + expect(mockTokenVaultContract.read.canonicalToBridged).not.toHaveBeenCalled(); + }); + + it('should return 0x0 if the native token is not bridged yet on the destination chain', async () => { + // Given + vi.mocked(mockTokenVaultContract.read.bridgedToCanonical).mockResolvedValue([destChainId, zeroAddress]); + vi.mocked(mockTokenVaultContract.read.canonicalToBridged).mockResolvedValue(zeroAddress); + // set the destination address it to zero so we can test it + mockToken.addresses[destChainId] = zeroAddress; + + // When + const result = await getCrossChainAddress({ + token: mockToken, + srcChainId, + destChainId, + }); + + // Then + expect(result).toEqual(zeroAddress); + expect(mockTokenVaultContract.read.bridgedToCanonical).toHaveBeenCalledWith([mockToken.addresses[srcChainId]]); + expect(mockTokenVaultContract.read.canonicalToBridged).toHaveBeenCalledWith([ + BigInt(srcChainId), + mockToken.addresses[srcChainId], + ]); + }); + + it('should return 0x0 if the token itself is bridged, but not to the destination chain', async () => { + // Given + vi.mocked(mockTokenVaultContract.read.bridgedToCanonical).mockResolvedValue([ + destChainId, + mockToken.addresses[srcChainId], + ]); + vi.mocked(mockTokenVaultContract.read.canonicalToBridged).mockResolvedValue(zeroAddress); + + // set the destination address it to zero so we can test it + mockToken.addresses[destChainId] = zeroAddress; + + // When + const result = await getCrossChainAddress({ + token: mockToken, + srcChainId, + destChainId, + }); + + // Then + expect(result).toEqual(zeroAddress); + expect(mockTokenVaultContract.read.bridgedToCanonical).toHaveBeenCalledWith([mockToken.addresses[srcChainId]]); + expect(mockTokenVaultContract.read.canonicalToBridged).toHaveBeenCalledWith([ + BigInt(destChainId), + mockToken.addresses[srcChainId], + ]); }); }); diff --git a/packages/bridge-ui-v2/src/libs/token/getCrossChainAddress.ts b/packages/bridge-ui-v2/src/libs/token/getCrossChainAddress.ts index b282a68432..b6fa27edac 100644 --- a/packages/bridge-ui-v2/src/libs/token/getCrossChainAddress.ts +++ b/packages/bridge-ui-v2/src/libs/token/getCrossChainAddress.ts @@ -1,32 +1,65 @@ -import { getContract } from '@wagmi/core'; +import { type Address, getContract } from '@wagmi/core'; +import { zeroAddress } from 'viem'; import { tokenVaultABI } from '$abi'; import { chainContractsMap } from '$libs/chain'; +import { getLogger } from '$libs/util/logger'; -import { type Token, TokenType } from './types'; +import { type GetCrossChainAddressArgs, TokenType } from './types'; -type GetCrossChainAddressArgs = { - token: Token; - srcChainId: number; - destChainId: number; -}; +const log = getLogger('token:getCrossChainAddress'); -export function getCrossChainAddress({ token, srcChainId, destChainId }: GetCrossChainAddressArgs) { - if (token.type === TokenType.ETH) return; // ETH doesn't have an address - - const { tokenVaultAddress } = chainContractsMap[destChainId]; +export async function getCrossChainAddress({ + token, + srcChainId, + destChainId, +}: GetCrossChainAddressArgs): Promise
{ + if (token.type === TokenType.ETH) return null; // ETH doesn't have an address + log( + `Getting cross chain address for token ${token.symbol} (${token.name}) from chain ${srcChainId} to chain ${destChainId}`, + ); const srcChainTokenAddress = token.addresses[srcChainId]; + const destChainTokenAddress = token.addresses[destChainId]; + + // check if we already have it + if (destChainTokenAddress && destChainTokenAddress !== zeroAddress) { + return token.addresses[destChainId]; + } - // We cannot find the address if we don't have - // the token address on the source chain - if (!srcChainTokenAddress) return; + if (!srcChainTokenAddress || srcChainTokenAddress === zeroAddress) { + throw new Error( + `Token ${token.symbol} (${token.name}) does not have any valid configured address on chain ${srcChainId} or ${destChainId}`, + ); + } + + const { tokenVaultAddress: srcChainTokenVaultAddress } = chainContractsMap[srcChainId]; + const { tokenVaultAddress: destChainTokenVaultAddress } = chainContractsMap[destChainId]; + + const srcTokenVaultContract = getContract({ + abi: tokenVaultABI, + chainId: srcChainId, + address: srcChainTokenVaultAddress, + }); const destTokenVaultContract = getContract({ abi: tokenVaultABI, chainId: destChainId, - address: tokenVaultAddress, + address: destChainTokenVaultAddress, }); - return destTokenVaultContract.read.canonicalToBridged([BigInt(srcChainId), srcChainTokenAddress]); + // first we need to figure out the canonical address of the token + const canonicalTokenInfo = await srcTokenVaultContract.read.bridgedToCanonical([srcChainTokenAddress]); + const canonicalTokenAddress = canonicalTokenInfo[1]; // this will break if the contracts ever change the order of the return values + + // if the canonical address is 0x0, then the token is canonical + if (canonicalTokenAddress === zeroAddress) { + // let's check if it is bridged on the destination chain by querying the destination vault + // e.g. bridged L1 -> L2 with native L1 token + return await destTokenVaultContract.read.canonicalToBridged([BigInt(srcChainId), srcChainTokenAddress]); + } else { + // if we have found a canonical, we can check for the bridged address on the source token vault + // e.g. bridging L2 -> L1 with native L1 token + return await srcTokenVaultContract.read.canonicalToBridged([BigInt(destChainId), canonicalTokenAddress]); + } } diff --git a/packages/bridge-ui-v2/src/libs/token/types.ts b/packages/bridge-ui-v2/src/libs/token/types.ts index 1fd4919904..bc3fd09a3e 100644 --- a/packages/bridge-ui-v2/src/libs/token/types.ts +++ b/packages/bridge-ui-v2/src/libs/token/types.ts @@ -29,6 +29,12 @@ export type TokenEnv = { balance: bigint; }; +export type GetCrossChainAddressArgs = { + token: Token; + srcChainId: number; + destChainId: number; +}; + export interface TokenService { storeToken(token: Token, address: string): Token[]; getTokens(address: string): Token[];