From 5730043130ddd6f3d2fb978f06d812435306e176 Mon Sep 17 00:00:00 2001 From: Bartek Date: Tue, 22 Oct 2024 17:39:15 +0200 Subject: [PATCH] test: add custom gas token e2e tests (#1960) --- .github/workflows/e2e-tests.yml | 19 +- .github/workflows/formatSpecfiles.js | 6 +- README.md | 6 + package.json | 3 +- .../arb-token-bridge-ui/src/pages/_app.tsx | 8 - .../arb-token-bridge-ui/src/pages/index.tsx | 54 +++-- .../src/util/TokenUtils.ts | 7 + .../arb-token-bridge-ui/src/util/networks.ts | 62 +++++- .../src/util/wagmi/wagmiAdditionalNetworks.ts | 12 +- .../arb-token-bridge-ui/synpress.config.ts | 209 +++++++++++++++--- .../tests/e2e/specfiles.json | 8 +- .../tests/e2e/specs/approveToken.cy.ts | 12 +- .../tests/e2e/specs/batchDeposit.cy.ts | 33 +-- .../tests/e2e/specs/depositCctp.cy.ts | 4 +- .../tests/e2e/specs/depositERC20.cy.ts | 28 ++- ...ositETH.cy.ts => depositNativeToken.cy.ts} | 18 +- .../tests/e2e/specs/importToken.cy.ts | 7 +- .../tests/e2e/specs/login.cy.ts | 35 ++- .../tests/e2e/specs/readClassicDeposits.cy.ts | 6 +- .../tests/e2e/specs/withdrawERC20.cy.ts | 21 +- .../tests/e2e/specs/withdrawETH.cy.ts | 148 ------------- .../tests/e2e/specs/withdrawNativeToken.cy.ts | 159 +++++++++++++ .../tests/support/commands.ts | 4 +- .../tests/support/common.ts | 42 +++- 24 files changed, 624 insertions(+), 287 deletions(-) rename packages/arb-token-bridge-ui/tests/e2e/specs/{depositETH.cy.ts => depositNativeToken.cy.ts} (73%) delete mode 100644 packages/arb-token-bridge-ui/tests/e2e/specs/withdrawETH.cy.ts create mode 100644 packages/arb-token-bridge-ui/tests/e2e/specs/withdrawNativeToken.cy.ts diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 298639ae9a..0c10ac5227 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -28,7 +28,7 @@ jobs: run: echo "e2eFiles=$(node .github/workflows/formatSpecfiles.js ${{ inputs.test_type }} | jq . --compact-output)" >> $GITHUB_OUTPUT test-e2e: - name: "Test E2E - ${{ matrix.test.name }}${{ matrix.test.type == 'orbit' && ' with L3' || ''}}" + name: "${{ matrix.test.name }}${{ matrix.test.type == 'orbit-eth' && ' with L3' || matrix.test.type == 'orbit-custom' && ' with custom fee token' || ''}}" needs: [load-e2e-files] runs-on: ubuntu-latest strategy: @@ -91,15 +91,22 @@ jobs: if: inputs.test_type != 'cctp' uses: OffchainLabs/actions/run-nitro-test-node@a20a76172ce524832ac897bef2fa10a62ed81c29 with: - nitro-testnode-ref: aab133aceadec2e622f15fa438f6327e3165392d - l3-node: ${{ matrix.test.type == 'orbit' }} - no-l3-token-bridge: ${{ matrix.test.type != 'orbit' }} + nitro-testnode-ref: badbcbea9b43d46e115da4d7c9f2f57c31af8431 + l3-node: ${{ matrix.test.type != 'regular' }} + no-l3-token-bridge: ${{ matrix.test.type == 'regular' }} + args: ${{ matrix.test.type == 'orbit-custom' && '--l3-fee-token' || '' }} - name: Run e2e tests via cypress-io/github-action uses: cypress-io/github-action@0da3c06ed8217b912deea9d8ee69630baed1737e # pin@v6.7.6 with: start: yarn start - command: "yarn test:e2e${{ (matrix.test.type == 'cctp' && ':cctp') || (matrix.test.type == 'orbit' && ':orbit') || '' }} --browser chrome" + command: >- + ${{ + (matrix.test.type == 'orbit-eth') && 'yarn test:e2e:orbit --browser chrome' || + (matrix.test.type == 'orbit-custom' && 'yarn test:e2e:orbit:custom-gas-token --browser chrome') || + (matrix.test.type == 'cctp' && 'yarn test:e2e:cctp --browser chrome') || + 'yarn test:e2e --browser chrome' + }} wait-on: http://127.0.0.1:3000 wait-on-timeout: 120 spec: ./packages/arb-token-bridge-ui/tests/e2e/specs/* @@ -121,7 +128,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: e2e-artifacts-${{ github.sha }}-${{ matrix.test.name }}-${{ (matrix.test.type == 'cctp' && 'cctp') || (matrix.test.type == 'orbit' && 'l3') || 'regular'}} + name: e2e-artifacts-${{ github.sha }}-${{ matrix.test.name }}-${{ (matrix.test.type == 'cctp' && 'cctp') || (matrix.test.type == 'orbit-eth' && 'l3') || (matrix.test.type == 'orbit-custom' && 'custom-fee-token') || 'regular'}} path: | ./packages/arb-token-bridge-ui/cypress/videos ./packages/arb-token-bridge-ui/cypress/screenshots diff --git a/.github/workflows/formatSpecfiles.js b/.github/workflows/formatSpecfiles.js index fac4b2f6c2..bb6b23e1ef 100644 --- a/.github/workflows/formatSpecfiles.js +++ b/.github/workflows/formatSpecfiles.js @@ -14,7 +14,11 @@ switch (testType) { }); tests.push({ ...spec, - type: "orbit", + type: "orbit-eth", + }); + tests.push({ + ...spec, + type: "orbit-custom", }); }); break; diff --git a/README.md b/README.md index 73f9acd030..74739c0054 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,12 @@ It is important for any code change to pass both unit and end-to-end tests. This ./test-node.bash --init --no-simple --tokenbridge --l3node --l3-token-bridge ``` + To run with a custom fee token also include the following flags: + + ```bash + --l3-fee-token --l3-fee-token-decimals 18 + ``` + 2. When the Nitro test-node is up and running you should see logs like `sequencer_1` and `staker-unsafe_1` in the terminal. This can take up to 10 minutes. 2. At the root of the token bridge UI: diff --git a/package.json b/package.json index c60ae31523..28b20b0721 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "lint:fix": "yarn workspace arb-token-bridge-ui lint:fix", "test:e2e": "yarn workspace arb-token-bridge-ui env-cmd --silent --file .e2e.env yarn synpress run --configFile synpress.config.ts", "test:e2e:cctp": "yarn test:e2e --configFile synpress.cctp.config.ts", - "test:e2e:orbit": "E2E_ORBIT=true yarn test:e2e" + "test:e2e:orbit": "E2E_ORBIT=true yarn test:e2e", + "test:e2e:orbit:custom-gas-token": "E2E_ORBIT_CUSTOM_GAS_TOKEN=true yarn test:e2e" }, "resolutions": { "**/@walletconnect/ethereum-provider": "2.13.1", diff --git a/packages/arb-token-bridge-ui/src/pages/_app.tsx b/packages/arb-token-bridge-ui/src/pages/_app.tsx index 54412f8abf..6fb6a7eca0 100644 --- a/packages/arb-token-bridge-ui/src/pages/_app.tsx +++ b/packages/arb-token-bridge-ui/src/pages/_app.tsx @@ -13,7 +13,6 @@ import 'tippy.js/themes/light.css' import '@rainbow-me/rainbowkit/styles.css' -import { registerLocalNetwork } from '../util/networks' import { Layout } from '../components/common/Layout' import { siteTitle } from './_document' @@ -21,13 +20,6 @@ import '../styles/tailwind.css' import '../styles/purple.css' import { isUserRejectedError } from '../util/isUserRejectedError' -if ( - process.env.NODE_ENV !== 'production' || - process.env.NEXT_PUBLIC_IS_E2E_TEST -) { - registerLocalNetwork() -} - dayjs.extend(utc) dayjs.extend(relativeTime) dayjs.extend(timeZone) diff --git a/packages/arb-token-bridge-ui/src/pages/index.tsx b/packages/arb-token-bridge-ui/src/pages/index.tsx index 1720c741f8..325bc493f4 100644 --- a/packages/arb-token-bridge-ui/src/pages/index.tsx +++ b/packages/arb-token-bridge-ui/src/pages/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React, { ComponentType, useEffect } from 'react' import { GetServerSidePropsContext, GetServerSidePropsResult } from 'next' import dynamic from 'next/dynamic' import { decodeString, encodeString } from 'use-query-params' @@ -7,7 +7,8 @@ import { registerCustomArbitrumNetwork } from '@arbitrum/sdk' import { Loader } from '../components/common/atoms/Loader' import { getCustomChainsFromLocalStorage, - mapCustomChainToNetworkData + mapCustomChainToNetworkData, + registerLocalNetwork } from '../util/networks' import { getOrbitChains } from '../util/orbitChainsList' import { sanitizeQueryParams } from '../hooks/useNetworks' @@ -17,17 +18,32 @@ import { } from '../hooks/useArbQueryParams' import { sanitizeExperimentalFeaturesQueryParam } from '../util' -const App = dynamic(() => import('../components/App/App'), { - ssr: false, - loading: () => ( - <> -
-
- -
- - ) -}) +const App = dynamic( + () => { + return new Promise<{ default: ComponentType }>(async resolve => { + if ( + process.env.NODE_ENV !== 'production' || + process.env.NEXT_PUBLIC_IS_E2E_TEST + ) { + await registerLocalNetwork() + } + + const AppComponent = await import('../components/App/App') + resolve(AppComponent) + }) + }, + { + ssr: false, + loading: () => ( + <> +
+
+ +
+ + ) + } +) function getDestinationWithSanitizedQueryParams( sanitized: { @@ -89,9 +105,11 @@ function addOrbitChainsToArbitrumSDK() { ) } -export function getServerSideProps({ +export async function getServerSideProps({ query -}: GetServerSidePropsContext): GetServerSidePropsResult> { +}: GetServerSidePropsContext): Promise< + GetServerSidePropsResult> +> { const sourceChainId = decodeChainQueryParam(query.sourceChain) const destinationChainId = decodeChainQueryParam(query.destinationChain) const experiments = decodeString(query.experiments) @@ -103,6 +121,12 @@ export function getServerSideProps({ } } + if ( + process.env.NODE_ENV !== 'production' || + process.env.NEXT_PUBLIC_IS_E2E_TEST + ) { + await registerLocalNetwork() + } // it's necessary to call this before sanitization to make sure all chains are registered addOrbitChainsToArbitrumSDK() diff --git a/packages/arb-token-bridge-ui/src/util/TokenUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenUtils.ts index 91318f35e7..99eaf03aaa 100644 --- a/packages/arb-token-bridge-ui/src/util/TokenUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/TokenUtils.ts @@ -66,6 +66,13 @@ function getErc20DataCache(params: GetErc20DataCacheParams): Erc20Data | null function getErc20DataCache( params?: GetErc20DataCacheParams ): Erc20DataCache | (Erc20Data | null) { + if ( + typeof window === 'undefined' || + typeof window.localStorage === 'undefined' + ) { + return null + } + const cache: Erc20DataCache = JSON.parse( // intentionally using || instead of ?? for it to work with an empty string localStorage.getItem(erc20DataCacheLocalStorageKey) || '{}' diff --git a/packages/arb-token-bridge-ui/src/util/networks.ts b/packages/arb-token-bridge-ui/src/util/networks.ts index 5dc8e5627c..02d26cdeb3 100644 --- a/packages/arb-token-bridge-ui/src/util/networks.ts +++ b/packages/arb-token-bridge-ui/src/util/networks.ts @@ -1,3 +1,4 @@ +import { StaticJsonRpcProvider } from '@ethersproject/providers' import { ArbitrumNetwork, getChildrenForNetwork, @@ -9,6 +10,7 @@ import { import { loadEnvironmentVariableWithFallback } from './index' import { getBridgeUiConfigForChain } from './bridgeUiConfig' import { chainIdToInfuraUrl } from './infura' +import { fetchErc20Data } from './TokenUtils' export enum ChainId { // L1 @@ -385,6 +387,42 @@ export const defaultL3Network: ArbitrumNetwork = { } } +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_LOCAL_ETHEREUM_RPC_URL, fallback: 'http://127.0.0.1:8545' @@ -398,14 +436,34 @@ export const localL3NetworkRpcUrl = loadEnvironmentVariableWithFallback({ fallback: 'http://127.0.0.1:3347' }) -export function registerLocalNetwork() { +export async function registerLocalNetwork() { try { rpcURLs[defaultL1Network.chainId] = localL1NetworkRpcUrl rpcURLs[defaultL2Network.chainId] = localL2NetworkRpcUrl rpcURLs[defaultL3Network.chainId] = localL3NetworkRpcUrl registerCustomArbitrumNetwork(defaultL2Network) - registerCustomArbitrumNetwork(defaultL3Network) + + let isLocalCustomNativeToken = false + + try { + const data = await fetchErc20Data({ + address: defaultL3CustomGasTokenNetwork.nativeToken!, + provider: new StaticJsonRpcProvider(localL2NetworkRpcUrl) + }) + if (data.symbol === 'TN') { + isLocalCustomNativeToken = true + } + } catch (e) { + // not the native token + isLocalCustomNativeToken = false + } + + registerCustomArbitrumNetwork( + isLocalCustomNativeToken + ? defaultL3CustomGasTokenNetwork + : defaultL3Network + ) } catch (error: any) { console.error(`Failed to register local network: ${error.message}`) } diff --git a/packages/arb-token-bridge-ui/src/util/wagmi/wagmiAdditionalNetworks.ts b/packages/arb-token-bridge-ui/src/util/wagmi/wagmiAdditionalNetworks.ts index 857f4f8bee..a241bcc182 100644 --- a/packages/arb-token-bridge-ui/src/util/wagmi/wagmiAdditionalNetworks.ts +++ b/packages/arb-token-bridge-ui/src/util/wagmi/wagmiAdditionalNetworks.ts @@ -5,7 +5,17 @@ import { ChainId, ChainWithRpcUrl, explorerUrls, rpcURLs } from '../networks' import { getBridgeUiConfigForChain } from '../bridgeUiConfig' export function chainToWagmiChain(chain: ChainWithRpcUrl): Chain { - const { nativeTokenData } = getBridgeUiConfigForChain(chain.chainId) + let { nativeTokenData } = getBridgeUiConfigForChain(chain.chainId) + + if (chain.chainId === ChainId.L3Local) { + nativeTokenData = chain.nativeToken + ? { + name: 'testnode', + symbol: 'TN', + decimals: 18 + } + : ether + } return { id: chain.chainId, diff --git a/packages/arb-token-bridge-ui/synpress.config.ts b/packages/arb-token-bridge-ui/synpress.config.ts index 7b85b3ede7..2150d747e3 100644 --- a/packages/arb-token-bridge-ui/synpress.config.ts +++ b/packages/arb-token-bridge-ui/synpress.config.ts @@ -10,8 +10,9 @@ import { formatUnits, parseUnits } from 'ethers/lib/utils' import { defineConfig } from 'cypress' import { StaticJsonRpcProvider } from '@ethersproject/providers' import synpressPlugins from '@synthetixio/synpress/plugins' +import { TestERC20__factory } from '@arbitrum/sdk/dist/lib/abi/factories/TestERC20__factory' import { TestWETH9__factory } from '@arbitrum/sdk/dist/lib/abi/factories/TestWETH9__factory' -import { Erc20Bridger } from '@arbitrum/sdk' +import { Erc20Bridger, EthBridger } from '@arbitrum/sdk' import logsPrinter from 'cypress-terminal-report/src/installLogsPrinter' import { getL2ERC20Address } from './src/util/TokenUtils' import specFiles from './tests/e2e/specfiles.json' @@ -19,7 +20,6 @@ import { contractAbi, contractByteCode } from './testErc20Token' import { checkForAssertions, generateActivityOnChains, - NetworkType, fundEth, setupCypressTasks, getCustomDestinationAddress, @@ -31,6 +31,7 @@ import { import { defaultL2Network, defaultL3Network, + defaultL3CustomGasTokenNetwork, registerLocalNetwork } from './src/util/networks' import { getCommonSynpressConfig } from './tests/e2e/getCommonSynpressConfig' @@ -39,19 +40,27 @@ const tests = process.env.TEST_FILE ? [process.env.TEST_FILE] : specFiles.map(file => file.file) -const isOrbitTest = process.env.E2E_ORBIT == 'true' +const isOrbitTest = [ + process.env.E2E_ORBIT, + process.env.E2E_ORBIT_CUSTOM_GAS_TOKEN +].includes('true') const shouldRecordVideo = process.env.CYPRESS_RECORD_VIDEO === 'true' +const l3Network = + process.env.ORBIT_CUSTOM_GAS_TOKEN === 'true' + ? defaultL3CustomGasTokenNetwork + : defaultL3Network + const l1WethGateway = isOrbitTest - ? defaultL3Network.tokenBridge!.parentWethGateway + ? l3Network.tokenBridge!.parentWethGateway : defaultL2Network.tokenBridge!.parentWethGateway -const l1WethAddress = isOrbitTest - ? defaultL3Network.tokenBridge!.parentWeth +let l1WethAddress = isOrbitTest + ? l3Network.tokenBridge!.parentWeth : defaultL2Network.tokenBridge!.parentWeth -const l2WethAddress = isOrbitTest - ? defaultL3Network.tokenBridge!.childWeth +let l2WethAddress = isOrbitTest + ? l3Network.tokenBridge!.childWeth : defaultL2Network.tokenBridge!.childWeth export default defineConfig({ @@ -59,7 +68,12 @@ export default defineConfig({ e2e: { async setupNodeEvents(on, config) { logsPrinter(on) - registerLocalNetwork() + + await registerLocalNetwork() + + const erc20Bridger = await Erc20Bridger.fromProvider(childProvider) + const ethBridger = await EthBridger.fromProvider(childProvider) + const isCustomFeeToken = isNonZeroAddress(ethBridger.nativeToken) if (!ethRpcUrl && !isOrbitTest) { throw new Error('NEXT_PUBLIC_LOCAL_ETHEREUM_RPC_URL variable missing.') @@ -98,14 +112,64 @@ export default defineConfig({ // Deploy and fund ERC20 to Parent and Child chains const l1ERC20Token = await deployERC20ToParentChain() + + // Approve custom fee token if not ETH + if (isCustomFeeToken) { + await approveCustomFeeToken({ + signer: localWallet.connect(parentProvider), + erc20ParentAddress: l1ERC20Token.address + }) + await approveCustomFeeToken({ + signer: localWallet.connect(parentProvider), + erc20ParentAddress: erc20Bridger.nativeToken! + }) + await ethBridger.approveGasToken({ + parentSigner: localWallet.connect(parentProvider) + }) + } + if (isCustomFeeToken) { + await fundUserWalletNativeCurrency() + } + await fundErc20ToParentChain(l1ERC20Token) - await fundErc20ToChildChain(l1ERC20Token) + await fundErc20ToChildChain({ + parentSigner: localWallet.connect(parentProvider), + parentErc20Address: l1ERC20Token.address, + amount: parseUnits('5', ERC20TokenDecimals), + isCustomFeeToken + }) await approveErc20(l1ERC20Token) + if (isCustomFeeToken) { + await approveCustomFeeToken({ + signer: userWallet.connect(parentProvider), + erc20ParentAddress: erc20Bridger.nativeToken! + }) + await ethBridger.approveGasToken({ + parentSigner: userWallet.connect(parentProvider) + }) + await erc20Bridger.approveGasToken({ + parentSigner: userWallet.connect(parentProvider), + erc20ParentAddress: l1WethAddress + }) + } + // Wrap ETH to test WETH transactions and approve it's usage - await fundWeth('parentChain') - await fundWeth('childChain') + await fundWethOnParentChain() await approveWeth() + if (isCustomFeeToken) { + await approveCustomFeeToken({ + signer: userWallet.connect(parentProvider), + erc20ParentAddress: l1WethAddress + }) + } + + await fundErc20ToChildChain({ + parentSigner: userWallet.connect(parentProvider), + parentErc20Address: l1WethAddress, + amount: utils.parseEther('0.1'), + isCustomFeeToken + }) // Generate activity on chains so that assertions get posted and claims can be made generateActivityOnChains({ @@ -114,7 +178,14 @@ export default defineConfig({ wallet: localWallet }) // Also keep watching assertions since they will act as a proof of activity and claims for withdrawals - checkForAssertions({ parentProvider, isOrbitTest }) + checkForAssertions({ + parentProvider, + testType: isCustomFeeToken + ? 'orbit-custom' + : process.env.E2E_ORBIT === 'true' + ? 'orbit-eth' + : 'regular' + }) // Set Cypress variables config.env.ETH_RPC_URL = isOrbitTest ? arbRpcUrl : ethRpcUrl @@ -127,6 +198,8 @@ export default defineConfig({ config.env.ERC20_TOKEN_ADDRESS_PARENT_CHAIN = l1ERC20Token.address config.env.LOCAL_WALLET_PRIVATE_KEY = localWallet.privateKey config.env.ORBIT_TEST = isOrbitTest ? '1' : '0' + config.env.NATIVE_TOKEN_SYMBOL = isCustomFeeToken ? 'TN' : 'ETH' + config.env.NATIVE_TOKEN_ADDRESS = ethBridger.nativeToken config.env.CUSTOM_DESTINATION_ADDRESS = await getCustomDestinationAddress() @@ -196,9 +269,58 @@ if (!process.env.PRIVATE_KEY_USER) { throw new Error('PRIVATE_KEY_USER variable missing.') } -const localWallet = new Wallet(process.env.PRIVATE_KEY_CUSTOM) +const localWallet = new Wallet( + process.env.E2E_ORBIT_CUSTOM_GAS_TOKEN === 'true' + ? utils.sha256(utils.toUtf8Bytes('user_fee_token_deployer')) + : process.env.PRIVATE_KEY_CUSTOM +) const userWallet = new Wallet(process.env.PRIVATE_KEY_USER) +async function approveCustomFeeToken({ + signer, + erc20ParentAddress, + amount +}: { + signer: Wallet + erc20ParentAddress: string + amount?: BigNumber +}) { + console.log('Approving custom fee token...') + const childErc20Bridger = await Erc20Bridger.fromProvider(childProvider) + + await childErc20Bridger.approveGasToken({ + parentSigner: signer, + erc20ParentAddress, + amount + }) +} + +async function fundUserWalletNativeCurrency() { + const childEthBridger = await EthBridger.fromProvider(childProvider) + + const address = await userWallet.getAddress() + + const tokenContract = TestERC20__factory.connect( + childEthBridger.nativeToken!, + localWallet.connect(parentProvider) + ) + + const userBalance = await tokenContract.balanceOf(address) + const shouldFund = userBalance.lt(utils.parseEther('0.3')) + + if (!shouldFund) { + console.log( + `User wallet has enough L3 native currency for testing, skip funding...` + ) + return + } + + console.log(`Funding native currency to user wallet on L2...`) + + const tx = await tokenContract.transfer(address, utils.parseEther('3')) + await tx.wait() +} + async function deployERC20ToParentChain() { console.log('Deploying ERC20...') const signer = localWallet.connect(parentProvider) @@ -219,6 +341,14 @@ async function deployERC20ToParentChain() { return l1TokenContract } +function isNonZeroAddress(address: string | undefined) { + return ( + typeof address === 'string' && + address !== constants.AddressZero && + utils.isAddress(address) + ) +} + async function deployERC20ToChildChain(erc20L1Address: string) { const bridger = await Erc20Bridger.fromProvider(childProvider) const deploy = await bridger.deposit({ @@ -228,6 +358,15 @@ async function deployERC20ToChildChain(erc20L1Address: string) { childProvider }) await deploy.wait() + + // store deployed weth address + if (erc20L1Address === l1WethAddress) { + l2WethAddress = await getL2ERC20Address({ + erc20L1Address: l1WethAddress, + l1Provider: parentProvider, + l2Provider: childProvider + }) + } } function getWethContract( @@ -237,14 +376,10 @@ function getWethContract( return TestWETH9__factory.connect(tokenAddress, userWallet.connect(provider)) } -async function fundWeth(networkType: NetworkType) { - console.log(`Funding WETH: ${networkType}...`) - const amount = networkType === 'parentChain' ? '0.2' : '0.1' - const address = networkType === 'parentChain' ? l1WethAddress : l2WethAddress - const provider = - networkType === 'parentChain' ? parentProvider : childProvider - const tx = await getWethContract(provider, address).deposit({ - value: utils.parseEther(amount) +async function fundWethOnParentChain() { + console.log(`Funding WETH...`) + const tx = await getWethContract(parentProvider, l1WethAddress).deposit({ + value: utils.parseEther('0.3') }) await tx.wait() } @@ -278,27 +413,39 @@ async function fundErc20ToParentChain(l1ERC20Token: Contract) { await transferTx.wait() } -async function fundErc20ToChildChain(l1ERC20Token: Contract) { - console.log('Funding ERC20 on Child Chain...') - // first deploy the ERC20 to L2 (if not, it might throw a gas error later) - await deployERC20ToChildChain(l1ERC20Token.address) +async function fundErc20ToChildChain({ + parentErc20Address, + parentSigner, + amount, + isCustomFeeToken +}: { + parentErc20Address: string + parentSigner: Wallet + amount: BigNumber + isCustomFeeToken: boolean +}) { + // deploy any token that's not WETH + // only deploy WETH for custom fee token chains because it's not deployed there + if (parentErc20Address !== l1WethAddress || isCustomFeeToken) { + // first deploy the ERC20 to L2 (if not, it might throw a gas error later) + await deployERC20ToChildChain(parentErc20Address) + } + const erc20Bridger = await Erc20Bridger.fromProvider(childProvider) - const parentSigner = localWallet.connect(parentProvider) // approve the ERC20 token for spending const approvalTx = await erc20Bridger.approveToken({ - erc20ParentAddress: l1ERC20Token.address, + erc20ParentAddress: parentErc20Address, parentSigner, amount: constants.MaxUint256 }) await approvalTx.wait() - // deposit the ERC20 token to L2 (fund the L2 account) const depositTx = await erc20Bridger.deposit({ parentSigner, childProvider, - erc20ParentAddress: l1ERC20Token.address, - amount: parseUnits('5', ERC20TokenDecimals), + erc20ParentAddress: parentErc20Address, + amount, destinationAddress: userWallet.address }) const depositRec = await depositTx.wait() diff --git a/packages/arb-token-bridge-ui/tests/e2e/specfiles.json b/packages/arb-token-bridge-ui/tests/e2e/specfiles.json index 8e740d8962..fd907011e2 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specfiles.json +++ b/packages/arb-token-bridge-ui/tests/e2e/specfiles.json @@ -5,13 +5,13 @@ "recordVideo": "false" }, { - "name": "Deposit ETH", - "file": "tests/e2e/specs/**/depositETH.cy.{js,jsx,ts,tsx}", + "name": "Deposit native token", + "file": "tests/e2e/specs/**/depositNativeToken.cy.{js,jsx,ts,tsx}", "recordVideo": "false" }, { - "name": "Withdraw ETH", - "file": "tests/e2e/specs/**/withdrawETH.cy.{js,jsx,ts,tsx}", + "name": "Withdraw native token", + "file": "tests/e2e/specs/**/withdrawNativeToken.cy.{js,jsx,ts,tsx}", "recordVideo": "false" }, { diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/approveToken.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/approveToken.cy.ts index ad6c3aa8c4..c96ceb903c 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/approveToken.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/approveToken.cy.ts @@ -2,7 +2,7 @@ import { importTokenThroughUI, ERC20TokenName, ERC20TokenSymbol, - zeroToLessThanOneETH, + getZeroToLessThanOneToken, getL1NetworkName, getL2NetworkName } from '../../support/common' @@ -11,6 +11,10 @@ const ERC20TokenAddressL1 = Cypress.env('ERC20_TOKEN_ADDRESS_PARENT_CHAIN') describe('Approve token for deposit', () => { // log in to metamask + const zeroToLessThanOneEth = getZeroToLessThanOneToken('ETH') + const zeroToLessThanOneNativeToken = getZeroToLessThanOneToken( + Cypress.env('NATIVE_TOKEN_SYMBOL') + ) it('should approve and deposit ERC-20 token', () => { context('Approve token', () => { @@ -26,9 +30,9 @@ describe('Approve token for deposit', () => { cy.findByText('MAX').click() - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeSummary(zeroToLessThanOneEth) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneEth) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneNativeToken) cy.waitUntil(() => cy.findMoveFundsButton().should('not.be.disabled'), { errorMsg: 'move funds button is disabled (expected to be enabled)', diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/batchDeposit.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/batchDeposit.cy.ts index 08f768bc31..6f002c0385 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/batchDeposit.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/batchDeposit.cy.ts @@ -6,7 +6,7 @@ import { getL1NetworkName, getL2NetworkConfig, getL2NetworkName, - zeroToLessThanOneETH + getZeroToLessThanOneToken } from '../../support/common' import { formatAmount } from '../../../src/util/NumberUtils' @@ -16,6 +16,11 @@ describe('Batch Deposit', () => { childNativeTokenBalance, childErc20Balance: string + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') + const zeroToLessThanOneEth = getZeroToLessThanOneToken('ETH') + const zeroToLessThanOneNativeToken = + getZeroToLessThanOneToken(nativeTokenSymbol) + beforeEach(() => { getInitialERC20Balance({ tokenAddress: Cypress.env('ERC20_TOKEN_ADDRESS_CHILD_CHAIN'), @@ -49,7 +54,7 @@ describe('Batch Deposit', () => { }) cy.findSourceChainButton(getL1NetworkName()) cy.findDestinationChainButton(getL2NetworkName()) - cy.findSelectTokenButton('ETH') + cy.findSelectTokenButton(nativeTokenSymbol) }) it('should deposit erc-20 and native currency to the same address', () => { @@ -81,7 +86,9 @@ describe('Batch Deposit', () => { }) context('native currency balance on child chain should not exist', () => { - cy.findByLabelText(`ETH balance amount on childChain`).should('not.exist') + cy.findByLabelText( + `${nativeTokenSymbol} balance amount on childChain` + ).should('not.exist') }) context('amount2 input should not exist', () => { @@ -99,7 +106,7 @@ describe('Batch Deposit', () => { }) context('native currency balance on child chain should show', () => { - cy.findByLabelText(`ETH balance amount on childChain`) + cy.findByLabelText(`${nativeTokenSymbol} balance amount on childChain`) .should('be.visible') .contains(childNativeTokenBalance) }) @@ -111,14 +118,14 @@ describe('Batch Deposit', () => { context('should show gas estimations and summary', () => { cy.typeAmount(ERC20AmountToSend) cy.typeAmount2(nativeCurrencyAmountToSend) - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeSummary(zeroToLessThanOneEth) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneEth) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneNativeToken) }) const txData = { symbol: ERC20TokenSymbol, - symbol2: 'ETH', + symbol2: nativeTokenSymbol, amount: ERC20AmountToSend, amount2: nativeCurrencyAmountToSend } @@ -154,7 +161,7 @@ describe('Batch Deposit', () => { .invoke('text') .then(parseFloat) .should('be.gt', Number(parentErc20Balance)) - cy.findByLabelText(`ETH balance amount on childChain`) + cy.findByLabelText(`${nativeTokenSymbol} balance amount on childChain`) .invoke('text') .then(parseFloat) .should( @@ -217,14 +224,14 @@ describe('Batch Deposit', () => { context('should show gas estimations and summary', () => { cy.typeAmount(ERC20AmountToSend) cy.typeAmount2(nativeCurrencyAmountToSend) - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeSummary(zeroToLessThanOneEth) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneEth) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneNativeToken) }) const txData = { symbol: ERC20TokenSymbol, - symbol2: 'ETH', + symbol2: nativeTokenSymbol, amount: ERC20AmountToSend, amount2: nativeCurrencyAmountToSend } diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts index f1d39bbf2d..f655a787ce 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/depositCctp.cy.ts @@ -2,9 +2,8 @@ * When user wants to bridge USDC through CCTP from L1 to L2 */ -import { zeroToLessThanOneETH } from '../../support/common' +import { getZeroToLessThanOneToken } from '../../support/common' import { CommonAddress } from '../../../src/util/CommonAddressUtils' -import { formatAmount } from 'packages/arb-token-bridge-ui/src/util/NumberUtils' // common function for this cctp deposit const confirmAndApproveCctpDeposit = () => { @@ -65,6 +64,7 @@ const confirmAndApproveCctpDeposit = () => { describe('Deposit USDC through CCTP', () => { // Happy Path const USDCAmountToSend = 0.0001 + const zeroToLessThanOneETH = getZeroToLessThanOneToken('ETH') beforeEach(() => { cy.login({ networkType: 'parentChain', networkName: 'sepolia' }) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/depositERC20.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/depositERC20.cy.ts index 2568bfbef9..1cd9ac5c13 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/depositERC20.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/depositERC20.cy.ts @@ -6,7 +6,7 @@ import { formatAmount } from '../../../src/util/NumberUtils' import { getInitialERC20Balance, getL1NetworkConfig, - zeroToLessThanOneETH, + getZeroToLessThanOneToken, moreThanZeroBalance, getL1NetworkName, getL2NetworkName, @@ -33,6 +33,10 @@ describe('Deposit Token', () => { const isOrbitTest = Cypress.env('ORBIT_TEST') == '1' const depositTime = isOrbitTest ? 'Less than a minute' : '9 minutes' + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') + const zeroToLessThanOneEth = getZeroToLessThanOneToken('ETH') + const zeroToLessThanOneNativeToken = + getZeroToLessThanOneToken(nativeTokenSymbol) // Happy Path Object.keys(depositTestCases).forEach(tokenType => { @@ -50,11 +54,11 @@ describe('Deposit Token', () => { }).then(val => (l1ERC20bal = formatAmount(val))) }) - it('should show L1 and L2 chains, and ETH correctly', () => { + it('should show L1 and L2 chains, and native token correctly', () => { cy.login({ networkType: 'parentChain' }) cy.findSourceChainButton(getL1NetworkName()) cy.findDestinationChainButton(getL2NetworkName()) - cy.findSelectTokenButton('ETH') + cy.findSelectTokenButton(nativeTokenSymbol) }) it(`should deposit ${tokenType} successfully to the same address`, () => { @@ -76,9 +80,12 @@ describe('Deposit Token', () => { context('should show gas estimations', () => { cy.typeAmount(ERC20AmountToSend) - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeSummary(zeroToLessThanOneEth) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneEth) + cy.findGasFeeForChain( + getL2NetworkName(), + zeroToLessThanOneNativeToken + ) }) context('should deposit successfully', () => { @@ -111,9 +118,12 @@ describe('Deposit Token', () => { context('should show summary', () => { cy.typeAmount(ERC20AmountToSend) - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeSummary(zeroToLessThanOneEth) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneEth) + cy.findGasFeeForChain( + getL2NetworkName(), + zeroToLessThanOneNativeToken + ) }) context('should fill custom destination address successfully', () => { diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/depositETH.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/depositNativeToken.cy.ts similarity index 73% rename from packages/arb-token-bridge-ui/tests/e2e/specs/depositETH.cy.ts rename to packages/arb-token-bridge-ui/tests/e2e/specs/depositNativeToken.cy.ts index 5de9ab52a4..7e674337b4 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/depositETH.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/depositNativeToken.cy.ts @@ -1,15 +1,19 @@ /** - * When user wants to bridge ETH from L1 to L2 + * When user wants to bridge native token from L1 to L2 */ import { getL1NetworkName, getL2NetworkName, - zeroToLessThanOneETH + getZeroToLessThanOneToken } from '../../support/common' -describe('Deposit ETH', () => { +describe('Deposit native token', () => { const ETHAmountToDeposit = 0.0001 + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') + const zeroToLessThanOneEth = getZeroToLessThanOneToken('ETH') + const zeroToLessThanOneNativeToken = + getZeroToLessThanOneToken(nativeTokenSymbol) const isOrbitTest = Cypress.env('ORBIT_TEST') == '1' const depositTime = isOrbitTest ? 'Less than a minute' : '9 minutes' @@ -24,15 +28,15 @@ describe('Deposit ETH', () => { it('should show gas estimations and bridge successfully', () => { cy.login({ networkType: 'parentChain' }) cy.typeAmount(ETHAmountToDeposit) - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeSummary(zeroToLessThanOneEth) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneEth) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneNativeToken) cy.findMoveFundsButton().click() cy.confirmMetamaskTransaction() cy.findTransactionInTransactionHistory({ duration: depositTime, amount: ETHAmountToDeposit, - symbol: 'ETH' + symbol: nativeTokenSymbol }) cy.closeTransactionHistoryPanel() cy.findAmountInput().should('have.value', '') diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts index 3946254dbf..86c712c350 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/importToken.cy.ts @@ -15,6 +15,7 @@ const ERC20TokenAddressL2: string = Cypress.env( ) describe('Import token', () => { + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') // we use mainnet to test token lists context('User import token through UI', () => { @@ -136,7 +137,7 @@ describe('Import token', () => { const addressWithoutLastChar = ERC20TokenAddressL1.slice(0, -1) // Remove the last character cy.login({ networkType: 'parentChain' }) - cy.findSelectTokenButton('ETH').click() + cy.findSelectTokenButton(nativeTokenSymbol).click() // open the Select Token popup cy.findByPlaceholderText(/Search by token name/i) @@ -246,6 +247,8 @@ describe('Import token', () => { visitAfterSomeDelay('/', { qs: { + sourceChain: 'arbitrum-localhost', + destinationChain: 'l3-localhost', token: invalidTokenAddress } }) @@ -261,7 +264,7 @@ describe('Import token', () => { .trigger('click', { force: true }) - cy.findSelectTokenButton('ETH') + cy.findSelectTokenButton(nativeTokenSymbol) // Modal is closed cy.findByRole('button', { name: 'Dialog Cancel' }).should('not.exist') diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/login.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/login.cy.ts index 0b6286f7e3..5c627e2344 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/login.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/login.cy.ts @@ -4,7 +4,9 @@ import { formatAmount } from '../../../src/util/NumberUtils' import { + getInitialERC20Balance, getInitialETHBalance, + getL1NetworkConfig, getL1NetworkName, getL2NetworkName } from './../../support/common' @@ -13,12 +15,24 @@ describe('Login Account', () => { let l1ETHbal let l2ETHbal + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') + const isCustomFeeToken = nativeTokenSymbol !== 'ETH' + before(() => { - getInitialETHBalance(Cypress.env('ETH_RPC_URL')).then( - val => (l1ETHbal = formatAmount(val)) - ) + if (isCustomFeeToken) { + getInitialERC20Balance({ + tokenAddress: Cypress.env('NATIVE_TOKEN_ADDRESS'), + multiCallerAddress: getL1NetworkConfig().multiCall, + address: Cypress.env('ADDRESS'), + rpcURL: Cypress.env('ETH_RPC_URL') + }).then(val => (l1ETHbal = formatAmount(val))) + } else { + getInitialETHBalance(Cypress.env('ETH_RPC_URL')).then( + val => (l1ETHbal = formatAmount(val)) + ) + } getInitialETHBalance(Cypress.env('ARB_RPC_URL')).then( - val => (l2ETHbal = formatAmount(val, { symbol: 'ETH' })) + val => (l2ETHbal = formatAmount(val)) ) }) @@ -33,16 +47,13 @@ describe('Login Account', () => { it('should connect wallet using MetaMask and display L1 and L2 balances', () => { cy.login({ networkType: 'parentChain' }) - // Balance: is in a different element so we check for siblings - cy.findByText(l1ETHbal) + cy.findByLabelText(`${nativeTokenSymbol} balance amount on parentChain`) .should('be.visible') - .siblings() - .contains('Balance: ') - // Balance: is in a different element so we check for siblings - cy.findByText(l2ETHbal) + .contains(l1ETHbal) + cy.findByLabelText(`${nativeTokenSymbol} balance amount on childChain`) .should('be.visible') - .siblings() - .contains('Balance: ') + .contains(l2ETHbal) + cy.findSourceChainButton(getL1NetworkName()) cy.findDestinationChainButton(getL2NetworkName()) }) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/readClassicDeposits.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/readClassicDeposits.cy.ts index 6725952d49..727778240c 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/readClassicDeposits.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/readClassicDeposits.cy.ts @@ -22,7 +22,7 @@ function mockClassicDepositTransaction( childChainId: 42161, status: 'success', isClassic: true, - assetName: 'ETH', + assetName: Cypress.env('NATIVE_TOKEN_SYMBOL'), assetType: AssetType.ETH, sender: Cypress.env('ADDRESS'), l1NetworkID: '1', @@ -44,8 +44,8 @@ describe('Read classic deposit messages', () => { window.localStorage.clear() }) - context('User has classic ETH deposit transaction', () => { - it('can read successful ETH deposit', () => { + context('User has classic native token deposit transaction', () => { + it('can read successful native token deposit', () => { // log in to metamask cy.login({ networkType: 'parentChain', diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawERC20.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawERC20.cy.ts index 54233ff35b..bbc0ff696f 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawERC20.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawERC20.cy.ts @@ -9,7 +9,7 @@ import { getL2NetworkConfig, getL1NetworkName, getL2NetworkName, - zeroToLessThanOneETH, + getZeroToLessThanOneToken, ERC20TokenSymbol } from '../../support/common' @@ -27,6 +27,9 @@ const withdrawalTestCases = { } describe('Withdraw ERC20 Token', () => { + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') + const zeroToLessThanOneNativeToken = + getZeroToLessThanOneToken(nativeTokenSymbol) let ERC20AmountToSend = Number((Math.random() * 0.001).toFixed(5)) // randomize the amount to be sure that previous transactions are not checked in e2e // when all of our tests need to run in a logged-in state // we have to make sure we preserve a healthy LocalStorage state @@ -70,7 +73,7 @@ describe('Withdraw ERC20 Token', () => { cy.findSourceChainButton(getL2NetworkName()) cy.findDestinationChainButton(getL1NetworkName()) cy.findMoveFundsButton().should('be.disabled') - cy.findSelectTokenButton('ETH') + cy.findSelectTokenButton(nativeTokenSymbol) }) it(`should withdraw ${tokenType} to the same address successfully`, () => { @@ -86,8 +89,11 @@ describe('Withdraw ERC20 Token', () => { context('should show summary', () => { cy.typeAmount(ERC20AmountToSend) - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeSummary(zeroToLessThanOneNativeToken) + cy.findGasFeeForChain( + getL2NetworkName(), + zeroToLessThanOneNativeToken + ) cy.findGasFeeForChain( new RegExp( `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, @@ -196,8 +202,11 @@ describe('Withdraw ERC20 Token', () => { context('should show summary', () => { cy.typeAmount(ERC20AmountToSend) - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeSummary(zeroToLessThanOneNativeToken) + cy.findGasFeeForChain( + getL2NetworkName(), + zeroToLessThanOneNativeToken + ) cy.findGasFeeForChain( new RegExp( `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawETH.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawETH.cy.ts deleted file mode 100644 index 1f45382c16..0000000000 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawETH.cy.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * When user wants to bridge ETH from L2 to L1 - */ - -import { - getInitialETHBalance, - getL1NetworkName, - getL2NetworkName, - zeroToLessThanOneETH -} from '../../support/common' -import { formatAmount } from '../../../src/util/NumberUtils' - -describe('Withdraw ETH', () => { - let ETHToWithdraw = Number((Math.random() * 0.001).toFixed(5)) // randomize the amount to be sure that previous transactions are not checked in e2e - let l1EthBal: string - - beforeEach(() => { - getInitialETHBalance( - Cypress.env('ETH_RPC_URL'), - Cypress.env('ADDRESS') - ).then( - val => - (l1EthBal = formatAmount(val, { - symbol: 'ETH' - })) - ) - }) - - // Happy Path - context('user has some ETH and is on L2', () => { - it('should show form fields correctly', () => { - cy.login({ networkType: 'childChain' }) - cy.findSourceChainButton(getL2NetworkName()) - cy.findDestinationChainButton(getL1NetworkName()) - cy.findMoveFundsButton().should('be.disabled') - }) - - context("bridge amount is lower than user's L2 ETH balance value", () => { - it('should show gas estimations', () => { - cy.login({ networkType: 'childChain' }) - cy.typeAmount(ETHToWithdraw) - cy.findGasFeeSummary(zeroToLessThanOneETH) - cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) - cy.findGasFeeForChain( - new RegExp( - `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, - 'i' - ) - ) - }) - - it('should show withdrawal confirmation and withdraw', () => { - ETHToWithdraw = Number((Math.random() * 0.001).toFixed(5)) // generate a new withdrawal amount for each test-run attempt so that findAllByText doesn't stall coz of prev transactions - cy.login({ networkType: 'childChain' }) - cy.typeAmount(ETHToWithdraw) - cy.findMoveFundsButton().click() - cy.findByText(/Arbitrum’s bridge/i).should('be.visible') - - // the Continue withdrawal button should be disabled at first - cy.findByRole('button', { - name: /Continue/i - }).should('be.disabled') - - cy.findByRole('switch', { - name: /before I can claim my funds/i - }) - .should('be.visible') - .click() - - cy.findByRole('switch', { - name: /after claiming my funds/i - }) - .should('be.visible') - .click() - // the Continue withdrawal button should not be disabled now - cy.findByRole('button', { - name: /Continue/i - }) - .should('be.enabled') - .click() - - cy.confirmMetamaskTransaction() - - cy.findTransactionInTransactionHistory({ - duration: 'an hour', - amount: ETHToWithdraw, - symbol: 'ETH' - }) - - context('transfer panel amount should be reset', () => { - cy.closeTransactionHistoryPanel() - cy.findAmountInput().should('have.value', '') - cy.findMoveFundsButton().should('be.disabled') - }) - }) - - it('should claim funds', { defaultCommandTimeout: 200_000 }, () => { - // increase the timeout for this test as claim button can take ~(20 blocks *10 blocks/sec) to activate - cy.login({ networkType: 'parentChain' }) // login to L1 to claim the funds (otherwise would need to change network after clicking on claim) - - cy.findByLabelText('Open Transaction History') - .should('be.visible') - .click() - - cy.findClaimButton( - formatAmount(ETHToWithdraw, { - symbol: 'ETH' - }) - ).click() - - cy.confirmMetamaskTransaction() - - cy.findByLabelText('show settled transactions') - .should('be.visible') - .click() - - cy.findByText( - `${formatAmount(ETHToWithdraw, { - symbol: 'ETH' - })}` - ).should('be.visible') - - cy.closeTransactionHistoryPanel() - - // the balance on the destination chain should not be the same as before - cy.findByLabelText('ETH balance amount on parentChain') - .should('be.visible') - .its('text') - .should('not.eq', l1EthBal) - }) - }) - - // TODO => test for bridge amount higher than user's L2 ETH balance - }) - - // TODO - will have both cases: - // 1. Arbitrum network is not added to metamask yet (add + switch) - // 2. Arbitrum network already configured in metamask (only switch) - context('user has some ETH and is on L1', () => {}) - // TODO - context('user has some ETH and is on wrong chain', () => {}) - // TODO - context('user has 0 ETH and is on L1', () => {}) - // TODO - context('user has 0 ETH and is on L2', () => {}) - // TODO - context('user has 0 ETH and is on wrong chain', () => {}) -}) diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawNativeToken.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawNativeToken.cy.ts new file mode 100644 index 0000000000..bee9061c24 --- /dev/null +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawNativeToken.cy.ts @@ -0,0 +1,159 @@ +/** + * When user wants to bridge native token from L2 to L1 + */ + +import { + getInitialETHBalance, + getL1NetworkName, + getL2NetworkName, + getZeroToLessThanOneToken +} from '../../support/common' +import { formatAmount } from '../../../src/util/NumberUtils' + +describe('Withdraw native token', () => { + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') + const zeroToLessThanOneNativeToken = + getZeroToLessThanOneToken(nativeTokenSymbol) + let ETHToWithdraw = Number((Math.random() * 0.001).toFixed(5)) // randomize the amount to be sure that previous transactions are not checked in e2e + let l1EthBal: string + + beforeEach(() => { + getInitialETHBalance( + Cypress.env('ETH_RPC_URL'), + Cypress.env('ADDRESS') + ).then( + val => + (l1EthBal = formatAmount(val, { + symbol: nativeTokenSymbol + })) + ) + }) + + // Happy Path + context('user has some native token and is on L2', () => { + it('should show form fields correctly', () => { + cy.login({ networkType: 'childChain' }) + cy.findSourceChainButton(getL2NetworkName()) + cy.findDestinationChainButton(getL1NetworkName()) + cy.findMoveFundsButton().should('be.disabled') + }) + + context( + "bridge amount is lower than user's L2 native token balance value", + () => { + it('should show gas estimations', () => { + cy.login({ networkType: 'childChain' }) + cy.typeAmount(ETHToWithdraw) + cy.findGasFeeSummary(zeroToLessThanOneNativeToken) + cy.findGasFeeForChain( + getL2NetworkName(), + zeroToLessThanOneNativeToken + ) + cy.findGasFeeForChain( + new RegExp( + `You'll have to pay ${getL1NetworkName()} gas fee upon claiming.`, + 'i' + ) + ) + }) + + it('should show withdrawal confirmation and withdraw', () => { + ETHToWithdraw = Number((Math.random() * 0.001).toFixed(5)) // generate a new withdrawal amount for each test-run attempt so that findAllByText doesn't stall coz of prev transactions + cy.login({ networkType: 'childChain' }) + cy.typeAmount(ETHToWithdraw) + cy.findMoveFundsButton().click() + cy.findByText(/Arbitrum’s bridge/i).should('be.visible') + + // the Continue withdrawal button should be disabled at first + cy.findByRole('button', { + name: /Continue/i + }).should('be.disabled') + + cy.findByRole('switch', { + name: /before I can claim my funds/i + }) + .should('be.visible') + .click() + + cy.findByRole('switch', { + name: /after claiming my funds/i + }) + .should('be.visible') + .click() + // the Continue withdrawal button should not be disabled now + cy.findByRole('button', { + name: /Continue/i + }) + .should('be.enabled') + .click() + + cy.confirmMetamaskTransaction() + + cy.findTransactionInTransactionHistory({ + duration: 'an hour', + amount: ETHToWithdraw, + symbol: nativeTokenSymbol + }) + + context('transfer panel amount should be reset', () => { + cy.closeTransactionHistoryPanel() + cy.findAmountInput().should('have.value', '') + cy.findMoveFundsButton().should('be.disabled') + }) + }) + + it('should claim funds', { defaultCommandTimeout: 200_000 }, () => { + // increase the timeout for this test as claim button can take ~(20 blocks *10 blocks/sec) to activate + cy.login({ networkType: 'parentChain' }) // login to L1 to claim the funds (otherwise would need to change network after clicking on claim) + + cy.findByLabelText('Open Transaction History') + .should('be.visible') + .click() + + cy.findClaimButton( + formatAmount(ETHToWithdraw, { + symbol: nativeTokenSymbol + }) + ).click() + + cy.confirmMetamaskTransaction() + + cy.findByLabelText('show settled transactions') + .should('be.visible') + .click() + + cy.findByText( + `${formatAmount(ETHToWithdraw, { + symbol: nativeTokenSymbol + })}` + ).should('be.visible') + + cy.closeTransactionHistoryPanel() + + // the balance on the destination chain should not be the same as before + cy.findByLabelText( + `${nativeTokenSymbol} balance amount on parentChain` + ) + .should('be.visible') + .its('text') + .should('not.eq', l1EthBal) + }) + } + ) + + // TODO => test for bridge amount higher than user's L2 ETH balance + }) + + // TODO - will have both cases: + // 1. Arbitrum network is not added to metamask yet (add + switch) + // 2. Arbitrum network already configured in metamask (only switch) + context('user has some ETH and is on L1', () => {}) + // TODO + context('user has some ETH and is on wrong chain', () => {}) + // TODO + context('user has 0 ETH and is on L1', () => {}) + // TODO + context('user has 0 ETH and is on L2', () => {}) + // TODO + context('user has 0 ETH and is on wrong chain', () => {}) +}) diff --git a/packages/arb-token-bridge-ui/tests/support/commands.ts b/packages/arb-token-bridge-ui/tests/support/commands.ts index 767668fb92..a3ae2a0dea 100644 --- a/packages/arb-token-bridge-ui/tests/support/commands.ts +++ b/packages/arb-token-bridge-ui/tests/support/commands.ts @@ -140,8 +140,8 @@ export const searchAndSelectToken = ({ tokenName: string tokenAddress: string }) => { - // Click on the ETH dropdown (Select token button) - cy.findSelectTokenButton('ETH').click() + // Click on the native token dropdown (Select token button) + cy.findSelectTokenButton(Cypress.env('NATIVE_TOKEN_SYMBOL') ?? 'ETH').click() // open the Select Token popup cy.findByPlaceholderText(/Search by token name/i) diff --git a/packages/arb-token-bridge-ui/tests/support/common.ts b/packages/arb-token-bridge-ui/tests/support/common.ts index 575bba6e4d..76664a8e91 100644 --- a/packages/arb-token-bridge-ui/tests/support/common.ts +++ b/packages/arb-token-bridge-ui/tests/support/common.ts @@ -6,7 +6,11 @@ import { Provider, StaticJsonRpcProvider } from '@ethersproject/providers' import { BigNumber, Signer, Wallet, ethers, utils } from 'ethers' import { MultiCaller } from '@arbitrum/sdk' import { MULTICALL_TESTNET_ADDRESS } from '../../src/constants' -import { defaultL2Network, defaultL3Network } from '../../src/util/networks' +import { + defaultL2Network, + defaultL3Network, + defaultL3CustomGasTokenNetwork +} from '../../src/util/networks' import { getChainIdFromProvider } from '../../src/token-bridge-sdk/utils' export type NetworkType = 'parentChain' | 'childChain' @@ -54,15 +58,21 @@ export const getL1NetworkConfig = (): NetworkConfig => { export const getL2NetworkConfig = (): NetworkConfig => { const isOrbitTest = Cypress.env('ORBIT_TEST') == '1' + const nativeTokenSymbol = Cypress.env('NATIVE_TOKEN_SYMBOL') ?? 'ETH' + const isCustomFeeToken = nativeTokenSymbol !== 'ETH' + + const l3Network = isCustomFeeToken + ? defaultL3CustomGasTokenNetwork + : defaultL3Network return { networkName: isOrbitTest ? 'l3-localhost' : 'arbitrum-localhost', rpcUrl: Cypress.env('ARB_RPC_URL'), chainId: isOrbitTest ? 333333 : 412346, - symbol: 'ETH', + symbol: nativeTokenSymbol, isTestnet: true, multiCall: isOrbitTest - ? defaultL3Network.tokenBridge!.childMultiCall + ? l3Network.tokenBridge!.childMultiCall : defaultL2Network.tokenBridge!.childMultiCall } } @@ -92,11 +102,14 @@ export const getL2TestnetNetworkConfig = (): NetworkConfig => { export const ERC20TokenName = 'Test Arbitrum Token' export const ERC20TokenSymbol = 'TESTARB' export const ERC20TokenDecimals = 18 -export const invalidTokenAddress = '0x0000000000000000000000000000000000000000' +export const invalidTokenAddress = utils.computeAddress(utils.randomBytes(32)) -export const zeroToLessThanOneETH = /0(\.\d+)*( ETH)/ export const moreThanZeroBalance = /0(\.\d+)/ +export function getZeroToLessThanOneToken(symbol: string) { + return new RegExp(`0(\\.\\d+)*( ${symbol})`) +} + export const importTokenThroughUI = (address: string) => { // Click on the ETH dropdown (Select token button) cy.findSelectTokenButton('ETH').click() @@ -229,19 +242,28 @@ export async function generateActivityOnChains({ export async function checkForAssertions({ parentProvider, - isOrbitTest + testType }: { parentProvider: Provider - isOrbitTest: boolean + testType: 'regular' | 'orbit-eth' | 'orbit-custom' }) { const abi = [ 'function latestConfirmed() public view returns (uint64)', 'function latestNodeCreated() public view returns (uint64)' ] - const rollupAddress = isOrbitTest - ? defaultL3Network.ethBridge.rollup - : defaultL2Network.ethBridge.rollup + let rollupAddress: string + + switch (testType) { + case 'orbit-eth': + rollupAddress = defaultL3Network.ethBridge.rollup + break + case 'orbit-custom': + rollupAddress = defaultL3CustomGasTokenNetwork.ethBridge.rollup + break + default: + rollupAddress = defaultL2Network.ethBridge.rollup + } const rollupContract = new ethers.Contract(rollupAddress, abi, parentProvider)