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)