diff --git a/.github/ISSUE_TEMPLATE/add-layerleap-request.yml b/.github/ISSUE_TEMPLATE/add-layerleap-request.yml index 2889fea0b3..798b9fc7a9 100644 --- a/.github/ISSUE_TEMPLATE/add-layerleap-request.yml +++ b/.github/ISSUE_TEMPLATE/add-layerleap-request.yml @@ -1,7 +1,7 @@ name: Add Layer Leap Request description: File a request to have your Orbit chain support Layer Leap transfers title: "[feat]: enable Layer Leap for " -labels: ["feat", "triage"] +labels: ["Type: Add LayerLeap"] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/add-orbit-chain-request.yml b/.github/ISSUE_TEMPLATE/add-orbit-chain-request.yml index eb3eb21fb6..75eebf1e8b 100644 --- a/.github/ISSUE_TEMPLATE/add-orbit-chain-request.yml +++ b/.github/ISSUE_TEMPLATE/add-orbit-chain-request.yml @@ -1,7 +1,7 @@ name: Add Orbit Chain Request description: File a request to have your Orbit chain added to the bridge title: "[feat]: Add Orbit chain " -labels: ["feat", "triage"] +labels: ["Type: Add Orbit Chain"] body: - type: markdown attributes: @@ -71,6 +71,8 @@ body: - "421614" - "11155111" - "17000" + - "8453" + - "84532" validations: required: true @@ -119,50 +121,20 @@ body: attributes: value: | Please fill out this section with your chain configuration details and contract addresses. As a reminder, you can get all contract addresses by running [getAllContracts](https://github.com/OffchainLabs/arbitrum-orbit-sdk/blob/feat-add-verification-scripts/examples/verify-rollup/README.md#get-all-contracts). - - type: input - id: confirmPeriodBlocks - attributes: - label: confirmPeriodBlocks - placeholder: ex. 45818 - validations: - required: true - type: markdown attributes: value: | - ## Rollup contract addresses + ## Rollup contract address - type: markdown attributes: value: | You can find this information under the `"coreContracts" : {...}` key. - - type: input - id: bridge - attributes: - label: bridge - validations: - required: true - - type: input - id: inbox - attributes: - label: inbox - validations: - required: true - - type: input - id: outbox - attributes: - label: outbox - validations: - required: true - type: input id: rollup attributes: label: rollup - validations: - required: true - - type: input - id: sequencerInbox - attributes: - label: sequencerInbox + description: Please provide the address of the rollup contract. Other core contract addresses will be fetched automatically. validations: required: true @@ -170,10 +142,6 @@ body: attributes: value: | ## Parent chain token bridge contract addresses - - type: markdown - attributes: - value: | - You can find this information under the `"l2Contracts" : {...}` key. - type: input id: parent-custom-gateway attributes: @@ -222,10 +190,6 @@ body: attributes: value: | ## Orbit chain token bridge contract addresses - - type: markdown - attributes: - value: | - You can find this information under the `"l3Contracts" : {...}` key. - type: input id: child-custom-gateway attributes: diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 3d60b8d978..e99b5f4bb6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -153,7 +153,7 @@ jobs: - name: Install node_modules uses: OffchainLabs/actions/node-modules/install@main - + - name: Build run: yarn workspace scripts build 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/audit-ci.jsonc b/audit-ci.jsonc index 52bdc29750..f5d48c759d 100644 --- a/audit-ci.jsonc +++ b/audit-ci.jsonc @@ -1,5 +1,13 @@ { "$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json", "low": true, - "allowlist": [] + "allowlist": [ + // https://github.com/advisories/GHSA-fc9h-whq2-v747 + // Valid ECDSA signatures erroneously rejected in Elliptic + // Legitimate transactions or communications may be incorrectly flagged as invalid. + // No patched version available yet + // from: arb-token-bridge-ui>@unstoppabledomains/resolution>elliptic + // from: arb-token-bridge-ui>ethers>@ethersproject/signing-key>elliptic + "GHSA-fc9h-whq2-v747" + ] } 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/.env.local.sample b/packages/arb-token-bridge-ui/.env.local.sample index f1b1d7ea73..9e0d6cfb67 100644 --- a/packages/arb-token-bridge-ui/.env.local.sample +++ b/packages/arb-token-bridge-ui/.env.local.sample @@ -8,8 +8,10 @@ NEXT_PUBLIC_INFURA_KEY_SEPOLIA= # L2 NEXT_PUBLIC_INFURA_KEY_ARBITRUM_ONE= +NEXT_PUBLIC_INFURA_KEY_BASE= # L2 Testnet NEXT_PUBLIC_INFURA_KEY_ARBITRUM_SEPOLIA= +NEXT_PUBLIC_INFURA_KEY_BASE_SEPOLIA= NEXT_PUBLIC_SENTRY_DSN= @@ -26,5 +28,6 @@ SELF_HOSTED_SUBGRAPH_API_KEY= SCREENING_API_ENDPOINT= SCREENING_API_KEY= +NEXT_PUBLIC_SCREENING_API_ENDPOINT= NEXT_PUBLIC_POSTHOG_KEY= diff --git a/packages/arb-token-bridge-ui/public/images/ApeChainLogo.svg b/packages/arb-token-bridge-ui/public/images/ApeChainLogo.svg new file mode 100644 index 0000000000..b5e6448444 --- /dev/null +++ b/packages/arb-token-bridge-ui/public/images/ApeChainLogo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/arb-token-bridge-ui/public/images/ApeTokenLogo.svg b/packages/arb-token-bridge-ui/public/images/ApeTokenLogo.svg new file mode 100644 index 0000000000..fd9175bb9d --- /dev/null +++ b/packages/arb-token-bridge-ui/public/images/ApeTokenLogo.svg @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/arb-token-bridge-ui/public/images/AtlasLogo.png b/packages/arb-token-bridge-ui/public/images/AtlasLogo.png deleted file mode 100644 index 6d256c9ab6..0000000000 Binary files a/packages/arb-token-bridge-ui/public/images/AtlasLogo.png and /dev/null differ diff --git a/packages/arb-token-bridge-ui/public/images/BaseWhite.svg b/packages/arb-token-bridge-ui/public/images/BaseWhite.svg new file mode 100644 index 0000000000..72e52ac2f8 --- /dev/null +++ b/packages/arb-token-bridge-ui/public/images/BaseWhite.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/arb-token-bridge-ui/public/images/GravityAlpha_Logo.png b/packages/arb-token-bridge-ui/public/images/GravityAlpha_Logo.png new file mode 100644 index 0000000000..891aec4023 Binary files /dev/null and b/packages/arb-token-bridge-ui/public/images/GravityAlpha_Logo.png differ diff --git a/packages/arb-token-bridge-ui/public/images/GravityAlpha_NativeTokenLogo.png b/packages/arb-token-bridge-ui/public/images/GravityAlpha_NativeTokenLogo.png new file mode 100644 index 0000000000..891aec4023 Binary files /dev/null and b/packages/arb-token-bridge-ui/public/images/GravityAlpha_NativeTokenLogo.png differ diff --git a/packages/arb-token-bridge-ui/public/images/PolterTestnetLogo.png b/packages/arb-token-bridge-ui/public/images/PolterTestnetLogo.png new file mode 100644 index 0000000000..70ee5c61fb Binary files /dev/null and b/packages/arb-token-bridge-ui/public/images/PolterTestnetLogo.png differ diff --git a/packages/arb-token-bridge-ui/public/images/ghst.png b/packages/arb-token-bridge-ui/public/images/ghst.png new file mode 100644 index 0000000000..b571b7eef9 Binary files /dev/null and b/packages/arb-token-bridge-ui/public/images/ghst.png differ diff --git a/packages/arb-token-bridge-ui/src/components/App/App.tsx b/packages/arb-token-bridge-ui/src/components/App/App.tsx index 65d3f60ccf..d23f339fb8 100644 --- a/packages/arb-token-bridge-ui/src/components/App/App.tsx +++ b/packages/arb-token-bridge-ui/src/components/App/App.tsx @@ -25,7 +25,7 @@ import { BalanceUpdater } from '../syncers/BalanceUpdater' import { TokenListSyncer } from '../syncers/TokenListSyncer' import { Header } from '../common/Header' import { HeaderAccountPopover } from '../common/HeaderAccountPopover' -import { getNetworkName, isNetwork } from '../../util/networks' +import { getNetworkName } from '../../util/networks' import { ArbQueryParamProvider, useArbQueryParams @@ -41,6 +41,7 @@ import { HeaderConnectWalletButton } from '../common/HeaderConnectWalletButton' import { onDisconnectHandler } from '../../util/walletConnectUtils' import { addressIsSmartContract } from '../../util/AddressUtils' import { useSyncConnectedChainToAnalytics } from './useSyncConnectedChainToAnalytics' +import { isDepositMode } from '../../util/isDepositMode' declare global { interface Window { @@ -98,15 +99,6 @@ const ArbTokenBridgeStoreSyncWrapper = (): JSX.Element | null => { // Any time one of those changes setTokenBridgeParams(null) actions.app.setConnectionState(ConnectionState.LOADING) - - const { - isArbitrum: isConnectedToArbitrum, - isOrbitChain: isConnectedToOrbitChain - } = isNetwork(networks.sourceChain.id) - const isParentChainEthereum = isNetwork( - parentChain.id - ).isEthereumMainnetOrTestnet - actions.app.reset(networks.sourceChain.id) actions.app.setChainIds({ l1NetworkChainId: parentChain.id, @@ -114,14 +106,16 @@ const ArbTokenBridgeStoreSyncWrapper = (): JSX.Element | null => { }) if ( - (isParentChainEthereum && isConnectedToArbitrum) || - isConnectedToOrbitChain + isDepositMode({ + sourceChainId: networks.sourceChain.id, + destinationChainId: networks.destinationChain.id + }) ) { - console.info('Withdrawal mode detected:') - actions.app.setConnectionState(ConnectionState.L2_CONNECTED) - } else { console.info('Deposit mode detected:') actions.app.setConnectionState(ConnectionState.L1_CONNECTED) + } else { + console.info('Withdrawal mode detected:') + actions.app.setConnectionState(ConnectionState.L2_CONNECTED) } setTokenBridgeParams({ diff --git a/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx b/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx index 95994fa261..522a69e6cb 100644 --- a/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx +++ b/packages/arb-token-bridge-ui/src/components/common/NetworkSelectionContainer.tsx @@ -32,10 +32,11 @@ import { useActions } from '../../state' import { useChainIdsForNetworkSelection } from '../../hooks/TransferPanel/useChainIdsForNetworkSelection' import { useAccountType } from '../../hooks/useAccountType' -type NetworkType = 'core' | 'orbit' +type NetworkType = 'core' | 'other' | 'orbit' enum ChainGroupName { core = 'CORE CHAINS', + other = 'OTHER CHAINS', orbit = 'ORBIT CHAINS' } @@ -48,6 +49,19 @@ const chainGroupInfo: { [key in NetworkType]: ChainGroupInfo } = { core: { name: ChainGroupName.core }, + other: { + name: ChainGroupName.other, + description: ( +

+ + + Independent projects using non-Arbitrum technology. These chains have + varying degrees of decentralization.{' '} + Bridge at your own risk. + +

+ ) + }, orbit: { name: ChainGroupName.orbit, description: ( @@ -71,7 +85,7 @@ function ChainTypeInfoRow({ style: CSSProperties }) { const { name, description } = chainGroup - const isCoreGroup = chainGroup.name === ChainGroupName.core + const isOrbitGroup = chainGroup.name === ChainGroupName.orbit return (
@@ -236,7 +250,11 @@ function NetworksPanel({ } const coreNetworks = chainIds.filter( - chainId => !isNetwork(chainId).isOrbitChain + chainId => isNetwork(chainId).isCoreChain + ) + const otherNetworks = chainIds.filter( + chainId => + !isNetwork(chainId).isCoreChain && !isNetwork(chainId).isOrbitChain ) const orbitNetworks = chainIds.filter( chainId => isNetwork(chainId).isOrbitChain @@ -244,6 +262,7 @@ function NetworksPanel({ return { core: coreNetworks, + other: otherNetworks, orbit: orbitNetworks } }, [debouncedNetworkSearched, chainIds]) @@ -262,6 +281,10 @@ function NetworksPanel({ groupedNetworks.push(ChainGroupName.core, ...networksToShow.core) } + if (networksToShow.other.length > 0) { + groupedNetworks.push(ChainGroupName.other, ...networksToShow.other) + } + if (networksToShow.orbit.length > 0) { groupedNetworks.push(ChainGroupName.orbit, ...networksToShow.orbit) } @@ -302,6 +325,12 @@ function NetworksPanel({ ) } + if (networkOrChainTypeName === ChainGroupName.other) { + return ( + + ) + } + if (networkOrChainTypeName === ChainGroupName.orbit) { return ( diff --git a/packages/arb-token-bridge-ui/src/hooks/__tests__/useNetworks.test.ts b/packages/arb-token-bridge-ui/src/hooks/__tests__/useNetworks.test.ts index 80bf5705ee..ec03fd1f9c 100644 --- a/packages/arb-token-bridge-ui/src/hooks/__tests__/useNetworks.test.ts +++ b/packages/arb-token-bridge-ui/src/hooks/__tests__/useNetworks.test.ts @@ -143,6 +143,18 @@ describe('sanitizeQueryParams', () => { }) }) }) + describe('when `destinationChainId` is valid but has no paired source chains and `sourceChainId` is undefined', () => { + it('should set `sourceChainId` to Ethereum and `destinationChainId` to Arbitrum One', () => { + const result = sanitizeQueryParams({ + sourceChainId: undefined, + destinationChainId: ChainId.Holesky + }) + expect(result).toEqual({ + sourceChainId: ChainId.Ethereum, + destinationChainId: ChainId.ArbitrumOne + }) + }) + }) describe('when `destinationChainId` is invalid and `sourceChainId` is valid', () => { it('should set `destinationChainId` based on `sourceChainId`', () => { @@ -207,4 +219,16 @@ describe('sanitizeQueryParams', () => { }) }) }) + describe('when `destinationChainId` is undefined and `sourceChainId` is valid but has no paired destination chains', () => { + it('should set `sourceChainId` to Ethereum and `destinationChainId` to Arbitrum One', () => { + const result = sanitizeQueryParams({ + sourceChainId: ChainId.Holesky, + destinationChainId: undefined + }) + expect(result).toEqual({ + sourceChainId: ChainId.Ethereum, + destinationChainId: ChainId.ArbitrumOne + }) + }) + }) }) diff --git a/packages/arb-token-bridge-ui/src/hooks/useAccountIsBlocked.ts b/packages/arb-token-bridge-ui/src/hooks/useAccountIsBlocked.ts index d77dd84e70..81f5a677d6 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useAccountIsBlocked.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useAccountIsBlocked.ts @@ -1,31 +1,49 @@ import { useMemo } from 'react' -import { useAccount, useNetwork } from 'wagmi' +import { useAccount } from 'wagmi' import useSWRImmutable from 'swr/immutable' -import { ApiResponseSuccess } from '../pages/api/screenings' import { trackEvent } from '../util/AnalyticsUtils' -import { isNetwork } from '../util/networks' import { Address } from '../util/AddressUtils' +import { captureSentryErrorWithExtraData } from '../util/SentryUtils' +/** + * Checks if an address is blocked using the external Screenings API service. + * @param {Address} address - The address to check. + * @returns {Promise} true if blocked or the request fails + */ async function isBlocked(address: Address): Promise { - if ( - process.env.NODE_ENV !== 'production' || - process.env.NEXT_PUBLIC_IS_E2E_TEST - ) { - return false - } + try { + if ( + process.env.NODE_ENV !== 'production' || + process.env.NEXT_PUBLIC_IS_E2E_TEST + ) { + return false + } + + const url = new URL(process.env.NEXT_PUBLIC_SCREENING_API_ENDPOINT ?? '') + url.searchParams.set('address', address) + + const response = await fetch(url, { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } - const searchParams = new URLSearchParams({ address }) - const response = await fetch('/api/screenings?' + searchParams, { - method: 'GET', - headers: { 'Content-Type': 'application/json' } - }) + const { blocked } = await response.json() + return blocked + } catch (error) { + console.error('Failed to check if address is blocked', error) + captureSentryErrorWithExtraData({ + error, + originFunction: 'isBlocked', + additionalData: { address } + }) - if (!response.ok) { return false } - - return ((await response.json()) as ApiResponseSuccess).blocked } async function fetcher(address: Address): Promise { @@ -40,7 +58,6 @@ async function fetcher(address: Address): Promise { export function useAccountIsBlocked() { const { address } = useAccount() - const { chain } = useNetwork() const queryKey = useMemo(() => { if (typeof address === 'undefined') { @@ -48,13 +65,8 @@ export function useAccountIsBlocked() { return null } - if (isNetwork(chain?.id ?? 0).isTestnet) { - // Don't fetch - return null - } - return [address.toLowerCase(), 'useAccountIsBlocked'] - }, [address, chain?.id]) + }, [address]) const { data: isBlocked } = useSWRImmutable( queryKey, diff --git a/packages/arb-token-bridge-ui/src/hooks/useNetworks.ts b/packages/arb-token-bridge-ui/src/hooks/useNetworks.ts index 64a394d3fb..b141f2d144 100644 --- a/packages/arb-token-bridge-ui/src/hooks/useNetworks.ts +++ b/packages/arb-token-bridge-ui/src/hooks/useNetworks.ts @@ -12,7 +12,9 @@ import { arbitrumSepolia, localL1Network as local, localL2Network as arbitrumLocal, - localL3Network as l3Local + localL3Network as l3Local, + base, + baseSepolia } from '../util/wagmi/wagmiAdditionalNetworks' import { getDestinationChainIds } from '../util/networks' @@ -37,7 +39,9 @@ export function isSupportedChainId( holesky.id, arbitrum.id, arbitrumNova.id, + base.id, arbitrumSepolia.id, + baseSepolia.id, arbitrumLocal.id, l3Local.id, local.id, @@ -74,7 +78,15 @@ export function sanitizeQueryParams({ isSupportedChainId(destinationChainId) ) { const [defaultSourceChainId] = getDestinationChainIds(destinationChainId) - return { sourceChainId: defaultSourceChainId!, destinationChainId } + + if (typeof defaultSourceChainId === 'undefined') { + return { + sourceChainId: ChainId.Ethereum, + destinationChainId: ChainId.ArbitrumOne + } + } + + return { sourceChainId: defaultSourceChainId, destinationChainId } } // sourceChainId is valid and destinationChainId is undefined @@ -83,9 +95,17 @@ export function sanitizeQueryParams({ !isSupportedChainId(destinationChainId) ) { const [defaultDestinationChainId] = getDestinationChainIds(sourceChainId) + + if (typeof defaultDestinationChainId === 'undefined') { + return { + sourceChainId: ChainId.Ethereum, + destinationChainId: ChainId.ArbitrumOne + } + } + return { sourceChainId: sourceChainId, - destinationChainId: defaultDestinationChainId! + destinationChainId: defaultDestinationChainId } } 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/token-bridge-sdk/utils.ts b/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts index 7dbf694b29..54e7483bbf 100644 --- a/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts +++ b/packages/arb-token-bridge-ui/src/token-bridge-sdk/utils.ts @@ -11,6 +11,7 @@ import { EthL1L3Bridger, getArbitrumNetwork } from '@arbitrum/sdk' +import { isDepositMode } from '../util/isDepositMode' export const getAddressFromSigner = async (signer: Signer) => { const address = await signer.getAddress() @@ -28,8 +29,6 @@ export const getBridgeTransferProperties = ( const sourceChainId = props.sourceChainId const destinationChainId = props.destinationChainId - const isSourceChainEthereumMainnetOrTestnet = - isNetwork(sourceChainId).isEthereumMainnetOrTestnet const isDestinationChainEthereumMainnetOrTestnet = isNetwork(destinationChainId).isEthereumMainnetOrTestnet @@ -37,16 +36,16 @@ export const getBridgeTransferProperties = ( const isDestinationChainArbitrum = isNetwork(destinationChainId).isArbitrum const isSourceChainOrbit = isNetwork(sourceChainId).isOrbitChain - const isDestinationChainOrbit = isNetwork(destinationChainId).isOrbitChain - const isDeposit = - isSourceChainEthereumMainnetOrTestnet || - (isSourceChainArbitrum && isDestinationChainOrbit) + const { isBase: isDestinationChainBase } = isNetwork(destinationChainId) + + const isDeposit = isDepositMode({ sourceChainId, destinationChainId }) const isWithdrawal = (isSourceChainArbitrum && isDestinationChainEthereumMainnetOrTestnet) || // l2 arbitrum chains to l1 (isSourceChainOrbit && isDestinationChainEthereumMainnetOrTestnet) || // l2 orbit chains to l1 - (isSourceChainOrbit && isDestinationChainArbitrum) // l3 orbit chains to l1 + (isSourceChainOrbit && isDestinationChainArbitrum) || // l3 orbit chains to l1 + (isSourceChainOrbit && isDestinationChainBase) // l3 orbit chain to Base l2 const isTeleport = isValidTeleportChainPair({ sourceChainId, diff --git a/packages/arb-token-bridge-ui/src/types/ChainQueryParam.ts b/packages/arb-token-bridge-ui/src/types/ChainQueryParam.ts index 6a9dde793a..77abdf0b20 100644 --- a/packages/arb-token-bridge-ui/src/types/ChainQueryParam.ts +++ b/packages/arb-token-bridge-ui/src/types/ChainQueryParam.ts @@ -13,9 +13,12 @@ import { chainToWagmiChain } from '../util/wagmi/wagmiAdditionalNetworks' const chainQueryParams = [ 'ethereum', 'sepolia', + 'holesky', 'arbitrum-one', 'arbitrum-nova', + 'base', 'arbitrum-sepolia', + 'base-sepolia', 'custom-localhost', 'arbitrum-localhost', 'l3-localhost' @@ -50,12 +53,21 @@ export function getChainQueryParamForChain(chainId: ChainId): ChainQueryParam { case ChainId.ArbitrumNova: return 'arbitrum-nova' + case ChainId.Base: + return 'base' + + case ChainId.Holesky: + return 'holesky' + case ChainId.Sepolia: return 'sepolia' case ChainId.ArbitrumSepolia: return 'arbitrum-sepolia' + case ChainId.BaseSepolia: + return 'base-sepolia' + case ChainId.Local: return 'custom-localhost' @@ -94,15 +106,24 @@ export function getChainForChainKeyQueryParam( case 'sepolia': return chains.sepolia + case 'holesky': + return customChains.holesky + case 'arbitrum-one': return chains.arbitrum case 'arbitrum-nova': return customChains.arbitrumNova + case 'base': + return customChains.base + case 'arbitrum-sepolia': return customChains.arbitrumSepolia + case 'base-sepolia': + return customChains.baseSepolia + case 'custom-localhost': return customChains.localL1Network 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/WithdrawOnlyUtils.ts b/packages/arb-token-bridge-ui/src/util/WithdrawOnlyUtils.ts index cf9f7d8aca..f08da0b4df 100644 --- a/packages/arb-token-bridge-ui/src/util/WithdrawOnlyUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/WithdrawOnlyUtils.ts @@ -205,6 +205,12 @@ export const withdrawOnlyTokens: { [chainId: number]: WithdrawOnlyToken[] } = { l2CustomAddr: '0x6985884c4392d348587b19cb9eaaf157f13271cd', l1Address: '0x6985884c4392d348587b19cb9eaaf157f13271cd', l2Address: '0xd99f14023f6bde3142d339b6c069b2b711da7e37' + }, + { + symbol: 'G3', + l2CustomAddr: '0xc24A365A870821EB83Fd216c9596eDD89479d8d7', + l1Address: '0xCF67815ccE72E682Eb4429eCa46843bed81Ca739', + l2Address: '0x34fb4148fdc1ab3054ac85d32de887c58538bb57' } ], [ChainId.ArbitrumNova]: [] diff --git a/packages/arb-token-bridge-ui/src/util/__tests__/networks.test.ts b/packages/arb-token-bridge-ui/src/util/__tests__/networks.test.ts index 554663bf91..2dbeb6f3cb 100644 --- a/packages/arb-token-bridge-ui/src/util/__tests__/networks.test.ts +++ b/packages/arb-token-bridge-ui/src/util/__tests__/networks.test.ts @@ -52,6 +52,15 @@ beforeAll(() => { }) registerCustomArbitrumNetwork(xaiTestnet) + + const polterTestnetChainId = 631571 + const polterTestnet = orbitTestnets[polterTestnetChainId] + + if (!polterTestnet) { + throw new Error(`Could not find Polter Testnet in the Orbit chains list.`) + } + + registerCustomArbitrumNetwork(polterTestnet) }) describe('getBaseChainIdByChainId', () => { @@ -254,4 +263,20 @@ describe('getDestinationChainIds', () => { expect(defaultChainId).toBe(ChainId.Sepolia) expect(isAscending(nonDefaultChainIds)).toBe(true) }) + + it('should return a sorted list for Base Sepolia', () => { + const destinationChainIds = getDestinationChainIds(ChainId.BaseSepolia) + const defaultChainId = destinationChainIds[0] + const nonDefaultChainIds = destinationChainIds.slice(1) + + expect(defaultChainId).toBe(631571) + expect(isAscending(nonDefaultChainIds)).toBe(true) + }) + + // Enable when there are Orbit Chains on Base + it('should not return a list for Base', () => { + const destinationChainIds = getDestinationChainIds(ChainId.Base) + + expect(destinationChainIds).toHaveLength(0) + }) }) diff --git a/packages/arb-token-bridge-ui/src/util/bridgeUiConfig.ts b/packages/arb-token-bridge-ui/src/util/bridgeUiConfig.ts index d68e0a1719..d7d4461918 100644 --- a/packages/arb-token-bridge-ui/src/util/bridgeUiConfig.ts +++ b/packages/arb-token-bridge-ui/src/util/bridgeUiConfig.ts @@ -104,6 +104,25 @@ export function getBridgeUiConfigForChain(chainId: number): BridgeUiConfig { 'AnyTrust protocol. Low fees for high-volume transactions. Secured by a trust-minimized Data Availability Committee (DAC).' } } + case ChainId.Base: + return { + color: '#0052ff', + network: { + name: 'Base', + logo: '/images/BaseWhite.svg', + description: + 'Base is an Optimistic Rollup built by Coinbase with the OP Stack.' + } + } + case ChainId.BaseSepolia: + return { + color: '#0052ff', + network: { + name: 'Base Sepolia', + logo: '/images/BaseWhite.svg', + description: 'Base Sepolia is an Ethereum L2 testnet by Coinbase.' + } + } default: { // added Orbit chains const orbitChain = orbitChains[chainId] diff --git a/packages/arb-token-bridge-ui/src/util/fastBridges.ts b/packages/arb-token-bridge-ui/src/util/fastBridges.ts index e30286313e..31e258336c 100644 --- a/packages/arb-token-bridge-ui/src/util/fastBridges.ts +++ b/packages/arb-token-bridge-ui/src/util/fastBridges.ts @@ -224,10 +224,10 @@ export const USDCFastBridges: USDCFastBridgeInfo[] = [ }: getHrefParams) => { switch (transferMode) { case 'deposit': - return `https://app.thevoyager.io/swap?fromChain=${from}&toChain=${to}&fromToken=${fromTokenAddress}&toToken=${toTokenAddress}` + return `https://app.routernitro.com/swap?fromChain=${from}&toChain=${to}&fromToken=${fromTokenAddress}&toToken=${toTokenAddress}` case 'withdraw': default: - return `https://app.thevoyager.io/swap?fromChain=${from}&toChain=${to}&fromToken=${fromTokenAddress}&toToken=${toTokenAddress}` + return `https://app.routernitro.com/swap?fromChain=${from}&toChain=${to}&fromToken=${fromTokenAddress}&toToken=${toTokenAddress}` } } } diff --git a/packages/arb-token-bridge-ui/src/util/infura.ts b/packages/arb-token-bridge-ui/src/util/infura.ts index 02124bb907..20fa95514d 100644 --- a/packages/arb-token-bridge-ui/src/util/infura.ts +++ b/packages/arb-token-bridge-ui/src/util/infura.ts @@ -61,10 +61,14 @@ export function chainIdToInfuraKey(chainId: ChainId) { return process.env.NEXT_PUBLIC_INFURA_KEY_SEPOLIA || defaultInfuraKey case ChainId.ArbitrumOne: return process.env.NEXT_PUBLIC_INFURA_KEY_ARBITRUM_ONE || defaultInfuraKey + case ChainId.Base: + return process.env.NEXT_PUBLIC_INFURA_KEY_BASE || defaultInfuraKey case ChainId.ArbitrumSepolia: return ( process.env.NEXT_PUBLIC_INFURA_KEY_ARBITRUM_SEPOLIA || defaultInfuraKey ) + case ChainId.BaseSepolia: + return process.env.NEXT_PUBLIC_INFURA_KEY_BASE_SEPOLIA || defaultInfuraKey default: return defaultInfuraKey @@ -81,8 +85,12 @@ export function chainIdToInfuraUrl(chainId: ChainId) { return `https://sepolia.infura.io/v3/${infuraKey}` case ChainId.ArbitrumOne: return `https://arbitrum-mainnet.infura.io/v3/${infuraKey}` + case ChainId.Base: + return `https://base-mainnet.infura.io/v3/${infuraKey}` case ChainId.ArbitrumSepolia: return `https://arbitrum-sepolia.infura.io/v3/${infuraKey}` + case ChainId.BaseSepolia: + return `https://base-sepolia.infura.io/v3/${infuraKey}` default: return undefined } diff --git a/packages/arb-token-bridge-ui/src/util/isDepositMode.ts b/packages/arb-token-bridge-ui/src/util/isDepositMode.ts index 86f49391ed..dd22cceb7c 100644 --- a/packages/arb-token-bridge-ui/src/util/isDepositMode.ts +++ b/packages/arb-token-bridge-ui/src/util/isDepositMode.ts @@ -9,13 +9,16 @@ export function isDepositMode({ }) { const { isEthereumMainnetOrTestnet: isSourceChainEthereum, - isArbitrum: isSourceChainArbitrum + isArbitrum: isSourceChainArbitrum, + isBase: isSourceChainBase } = isNetwork(sourceChainId) const { isOrbitChain: isDestinationChainOrbit } = isNetwork(destinationChainId) const isDepositMode = - isSourceChainEthereum || (isSourceChainArbitrum && isDestinationChainOrbit) + isSourceChainEthereum || + isSourceChainBase || + (isSourceChainArbitrum && isDestinationChainOrbit) return isDepositMode } diff --git a/packages/arb-token-bridge-ui/src/util/networks.ts b/packages/arb-token-bridge-ui/src/util/networks.ts index e6b22e9a83..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 @@ -20,20 +22,23 @@ export enum ChainId { // L2 ArbitrumOne = 42161, ArbitrumNova = 42170, + Base = 8453, // L2 Testnets ArbitrumSepolia = 421614, ArbitrumLocal = 412346, + BaseSepolia = 84532, // L3 Testnets L3Local = 333333 } -type L1Network = { +/** The network that you reference when calling `block.number` in solidity */ +type BlockNumberReferenceNetwork = { chainId: ChainId blockTime: number isTestnet: boolean } -const l1Networks: { [chainId: number]: L1Network } = { +const l1Networks: { [chainId: number]: BlockNumberReferenceNetwork } = { [ChainId.Ethereum]: { chainId: ChainId.Ethereum, blockTime: 12, @@ -56,12 +61,32 @@ const l1Networks: { [chainId: number]: L1Network } = { } } +const baseNetworks: { [chainId: number]: BlockNumberReferenceNetwork } = { + [ChainId.Base]: { + chainId: ChainId.Base, + blockTime: 2, + isTestnet: false + }, + [ChainId.BaseSepolia]: { + chainId: ChainId.BaseSepolia, + blockTime: 2, + isTestnet: true + } +} + export const getChains = () => { - const chains = [...Object.values(l1Networks), ...getArbitrumNetworks()] + const chains: (BlockNumberReferenceNetwork | ArbitrumNetwork)[] = [ + ...Object.values(l1Networks), + ...Object.values(baseNetworks), + ...getArbitrumNetworks() + ] return chains.filter(chain => { - // exclude L1 chains with no child chains - if (isL1Chain(chain) && getChildrenForNetwork(chain.chainId).length === 0) { + // exclude L1 chains or Base Chains with no child chains + if ( + isBlockNumberReferenceNetwork(chain) && + getChildrenForNetwork(chain.chainId).length === 0 + ) { return false } @@ -86,12 +111,12 @@ export function getBaseChainIdByChainId({ }: { chainId: number }): number { - // the chain provided is an L1 chain, so we can return early - if (isL1Chain({ chainId })) { + // the chain provided is an L1 chain or Base chain, so we can return early + if (isBlockNumberReferenceNetwork({ chainId })) { return chainId } - let currentParentChain: L1Network | ArbitrumNetwork + let currentParentChain: BlockNumberReferenceNetwork | ArbitrumNetwork try { currentParentChain = getArbitrumNetwork(chainId) @@ -99,9 +124,9 @@ export function getBaseChainIdByChainId({ return chainId } - // keep following the parent chains until we find the L1 chain + // keep following the parent chains until we find the L1/Base chain while (true) { - if (isL1Chain(currentParentChain)) { + if (isBlockNumberReferenceNetwork(currentParentChain)) { return currentParentChain.chainId } @@ -184,10 +209,12 @@ export function removeCustomChainFromLocalStorage(chainId: number) { ) } +// Only support testnet chains export const supportedCustomOrbitParentChains = [ ChainId.Sepolia, ChainId.Holesky, - ChainId.ArbitrumSepolia + ChainId.ArbitrumSepolia, + ChainId.BaseSepolia ] export const rpcURLs: { [chainId: number]: string } = { @@ -208,10 +235,18 @@ export const rpcURLs: { [chainId: number]: string } = { fallback: 'https://arb1.arbitrum.io/rpc' }), [ChainId.ArbitrumNova]: 'https://nova.arbitrum.io/rpc', + [ChainId.Base]: loadEnvironmentVariableWithFallback({ + env: chainIdToInfuraUrl(ChainId.Base), + fallback: 'https://mainnet.base.org' + }), // L2 Testnets [ChainId.ArbitrumSepolia]: loadEnvironmentVariableWithFallback({ env: chainIdToInfuraUrl(ChainId.ArbitrumSepolia), fallback: 'https://sepolia-rollup.arbitrum.io/rpc' + }), + [ChainId.BaseSepolia]: loadEnvironmentVariableWithFallback({ + env: chainIdToInfuraUrl(ChainId.BaseSepolia), + fallback: 'https://sepolia.base.org' }) } @@ -224,8 +259,10 @@ export const explorerUrls: { [chainId: number]: string } = { // L2 [ChainId.ArbitrumNova]: 'https://nova.arbiscan.io', [ChainId.ArbitrumOne]: 'https://arbiscan.io', + [ChainId.Base]: 'https://basescan.org', // L2 Testnets - [ChainId.ArbitrumSepolia]: 'https://sepolia.arbiscan.io' + [ChainId.ArbitrumSepolia]: 'https://sepolia.arbiscan.io', + [ChainId.BaseSepolia]: 'https://sepolia.basescan.org' } export const getExplorerUrl = (chainId: ChainId) => { @@ -236,7 +273,7 @@ export const getExplorerUrl = (chainId: ChainId) => { export const getL1BlockTime = (chainId: number) => { const chain = getChainByChainId(getBaseChainIdByChainId({ chainId })) - if (!chain || !isL1Chain(chain)) { + if (!chain || !isBlockNumberReferenceNetwork(chain)) { throw new Error(`Couldn't get block time. Unexpected chain ID: ${chainId}`) } @@ -244,6 +281,11 @@ export const getL1BlockTime = (chainId: number) => { } export const getConfirmPeriodBlocks = (chainId: ChainId) => { + // Base is not an Arbitrum chain so it doesn't work in the same way, and we don't support deposits from L1, or withdrawals from Base chains + if (isNetwork(chainId).isBase) { + return 0 + } + return getArbitrumNetwork(chainId).confirmPeriodBlocks } @@ -269,7 +311,7 @@ export const l2MoonGatewayAddresses: { [chainId: number]: string } = { [ChainId.ArbitrumNova]: '0xA430a792c14d3E49d9D00FD7B4BA343F516fbB81' } -const defaultL1Network: L1Network = { +const defaultL1Network: BlockNumberReferenceNetwork = { blockTime: 10, chainId: 1337, isTestnet: true @@ -345,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' @@ -358,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}`) } @@ -377,6 +475,11 @@ function isTestnetChain(chainId: ChainId) { return l1Network.isTestnet } + const baseNetwork = baseNetworks[chainId] + if (baseNetwork) { + return baseNetwork.isTestnet + } + try { return getArbitrumNetwork(chainId).isTestnet } catch { @@ -385,6 +488,14 @@ function isTestnetChain(chainId: ChainId) { } } +function getIsArbitrumChain(chainId: ChainId) { + try { + return !!getArbitrumNetwork(chainId).parentChainId + } catch (error) { + return false + } +} + export function isNetwork(chainId: ChainId) { const isEthereumMainnet = chainId === ChainId.Ethereum @@ -397,14 +508,19 @@ export function isNetwork(chainId: ChainId) { const isArbitrumSepolia = chainId === ChainId.ArbitrumSepolia const isArbitrumLocal = chainId === ChainId.ArbitrumLocal + const isBaseMainnet = chainId === ChainId.Base + const isBaseSepolia = chainId === ChainId.BaseSepolia + const isEthereumMainnetOrTestnet = isEthereumMainnet || isSepolia || isHolesky || isLocal const isArbitrum = isArbitrumOne || isArbitrumNova || isArbitrumLocal || isArbitrumSepolia + const isBase = isBaseMainnet || isBaseSepolia + const isCoreChain = isEthereumMainnetOrTestnet || isArbitrum - const isOrbitChain = !isCoreChain + const isOrbitChain = getIsArbitrumChain(chainId) && !isCoreChain return { // L1 @@ -416,8 +532,11 @@ export function isNetwork(chainId: ChainId) { isArbitrum, isArbitrumOne, isArbitrumNova, + isBase, + isBaseMainnet, // L2 Testnets isArbitrumSepolia, + isBaseSepolia, // Orbit chains isOrbitChain, // General @@ -464,22 +583,29 @@ export function mapCustomChainToNetworkData(chain: ChainWithRpcUrl) { explorerUrls[chain.chainId] = chain.explorerUrl } -function isL1Chain(chain: { chainId: number }): chain is L1Network { - return typeof l1Networks[chain.chainId] !== 'undefined' -} - function isArbitrumChain( - chain: L1Network | ArbitrumNetwork + chain: BlockNumberReferenceNetwork | ArbitrumNetwork ): chain is ArbitrumNetwork { return typeof (chain as ArbitrumNetwork).parentChainId !== 'undefined' } +function isBlockNumberReferenceNetwork(chain: { + chainId: number +}): chain is BlockNumberReferenceNetwork { + return ( + typeof l1Networks[chain.chainId] !== 'undefined' || + typeof baseNetworks[chain.chainId] !== 'undefined' + ) +} + export const TELEPORT_ALLOWLIST: { [id: number]: number[] } = { [ChainId.Ethereum]: [1380012617, 70700, 70701], // Rari, PopApex and PopBoss [ChainId.Sepolia]: [1918988905] // RARI Testnet } -export function getChildChainIds(chain: ArbitrumNetwork | L1Network) { +export function getChildChainIds( + chain: ArbitrumNetwork | BlockNumberReferenceNetwork +) { const childChainIds = [ ...getChildrenForNetwork(chain.chainId).map(chain => chain.chainId), ...(TELEPORT_ALLOWLIST[chain.chainId] ?? []) // for considering teleport (L1-L3 transfers) we will get the L3 children of the chain, if present diff --git a/packages/arb-token-bridge-ui/src/util/orbitChainsData.json b/packages/arb-token-bridge-ui/src/util/orbitChainsData.json index 06dc26ad1d..6a849f5bb4 100644 --- a/packages/arb-token-bridge-ui/src/util/orbitChainsData.json +++ b/packages/arb-token-bridge-ui/src/util/orbitChainsData.json @@ -491,6 +491,107 @@ "logoUrl": "/images/sxTokenLogo.png" } } + }, + { + "chainId": 33139, + "confirmPeriodBlocks": 45818, + "ethBridge": { + "bridge": "0x6B71AFb4b7725227ab944c96FE018AB9dc0434b8", + "inbox": "0x1B98e4ED82Ee1a91A65a38C690e2266364064D15", + "outbox": "0x4F405BA65291063d8A524c2bDf55d4e67405c2aF", + "rollup": "0x374de579AE15aD59eD0519aeAf1A23F348Df259c", + "sequencerInbox": "0xE6a92Ae29E24C343eE66A2B3D3ECB783d65E4a3C" + }, + "explorerUrl": "https://apescan.io", + "nativeToken": "0x7f9FBf9bDd3F4105C478b996B648FE6e828a1e98", + "rpcUrl": "https://apechain.calderachain.xyz/http", + "isCustom": true, + "isTestnet": false, + "name": "ApeChain", + "slug": "apechain", + "parentChainId": 42161, + "retryableLifetimeSeconds": 604800, + "tokenBridge": { + "parentCustomGateway": "0xEd543dA6fE33cceE50DC024b78C27959235D0ab0", + "parentErc20Gateway": "0xB603a1C07A11945bFe4855347c88583e31b8ddB0", + "parentGatewayRouter": "0xD57Df5C2Dc2D60307a74944191f2aA5B7BD4a700", + "parentMultiCall": "0x411f8A148e448bBe75382d4FFABee0796484f3c6", + "parentProxyAdmin": "0x1E5f8ff72895aEa53DD62b590dA51E92dC75b507", + "parentWeth": "0x0000000000000000000000000000000000000000", + "parentWethGateway": "0x0000000000000000000000000000000000000000", + "childCustomGateway": "0x49607221AF038229cEc6c85DB37a652E00226D29", + "childErc20Gateway": "0xf9710169D2f3524E7Bf068F6DDF6b2cc65D38a14", + "childGatewayRouter": "0x31eFB847d3f7A0C9Ba1364a6423aEa0a7a60BF3C", + "childMultiCall": "0x350186513FB2C148A50439311533656EF8096D9C", + "childProxyAdmin": "0x965a8220D41A031b1F71d94feFBf13dE4f686B33", + "childWeth": "0x0000000000000000000000000000000000000000", + "childWethGateway": "0x0000000000000000000000000000000000000000" + }, + "bridgeUiConfig": { + "color": "#0054FA", + "network": { + "name": "ApeChain", + "logo": "/images/ApeChainLogo.svg", + "description": "ApeChain is a dedicated infrastructure layer to power the ApeCoin ecosystem." + }, + "nativeTokenData": { + "name": "ApeCoin", + "symbol": "APE", + "decimals": 18, + "logoUrl": "/images/ApeTokenLogo.svg" + } + } + }, + { + "chainId": 1625, + "confirmPeriodBlocks": 40320, + "ethBridge": { + "bridge": "0x7983403dDA368AA7d67145a9b81c5c517F364c42", + "inbox": "0x7AD2a94BefF3294a31894cFb5ba4206957a53c19", + "outbox": "0x1153a1e4B1523DFf36f77d696bd6eBF2B0e7DAbF", + "rollup": "0xf993AF239770932A0EDaB88B6A5ba3708Bd58239", + "sequencerInbox": "0x8D99372612e8cFE7163B1a453831Bc40eAeb3cF3" + }, + "nativeToken": "0x9C7BEBa8F6eF6643aBd725e45a4E8387eF260649", + "explorerUrl": "https://explorer.gravity.xyz", + "rpcUrl": "https://rpc.gravity.xyz", + "isArbitrum": true, + "isCustom": true, + "isTestnet": false, + "name": "Gravity Alpha", + "slug": "gravity-alpha", + "parentChainId": 1, + "retryableLifetimeSeconds": 604800, + "tokenBridge": { + "parentCustomGateway": "0xa26Fd1c23634870303e42311E114D5cc8301Ed1E", + "parentErc20Gateway": "0xb23988D9728EF147EAa02D602D7e067B6131A1bB", + "parentGatewayRouter": "0x8713569d016f981D956715e9EE2795382168b5c0", + "parentMultiCall": "0x7cdCB0Cc61f47B8Dd8f47C5A29edaDd84a1BDf5e", + "parentProxyAdmin": "0xBbc3872E30C91ef69336937838c2a283F79f7E68", + "parentWeth": "0x0000000000000000000000000000000000000000", + "parentWethGateway": "0x0000000000000000000000000000000000000000", + "childCustomGateway": "0xC18EADE2B2CdA6AcFAc4fd2226C724a1008b02Ab", + "childErc20Gateway": "0xD330E617270F375Bd476896f3A8AE9041264E13d", + "childGatewayRouter": "0xf1cA401FB474520EbaBb285670891dEbd7C505Bc", + "childMultiCall": "0xABF31e3A13528082cE5bb05D6E88749556DAFD5F", + "childProxyAdmin": "0xB881cf085a78491AaA71Bf22bc87E67865a4409a", + "childWeth": "0x0000000000000000000000000000000000000000", + "childWethGateway": "0x0000000000000000000000000000000000000000" + }, + "bridgeUiConfig": { + "color": "#FFAC43", + "network": { + "name": "Gravity Alpha", + "logo": "/images/GravityAlpha_Logo.png", + "description": "The Magnet Chain for Web3." + }, + "nativeTokenData": { + "name": "G", + "symbol": "G", + "decimals": 18, + "logoUrl": "/images/GravityAlpha_NativeTokenLogo.png" + } + } } ], "testnet": [ @@ -682,56 +783,6 @@ "fastWithdrawalActive": true } }, - { - "chainId": 1183, - "confirmPeriodBlocks": 150, - "ethBridge": { - "bridge": "0xCAeCF7c5c2769e250Ef69592dD470D40BC61D1f7", - "inbox": "0x44199D537Aa690bE3B4446602e0c66F2B992D51e", - "outbox": "0xAD6A8df458dBD6b440F50313C5d53E3C5b4734e9", - "rollup": "0x357717a0F9Ac8714A5995109Fba3BAd3b5f0954F", - "sequencerInbox": "0x146bCf344F949ad4c9Fcf339eb4415981aE21dF9" - }, - "nativeToken": "0x566f8345F7bF45358FaB2802C19c60D691dE04e4", - "explorerUrl": "https://testnet.theatlas.tech", - "rpcUrl": "https://theatlas.tech", - "isCustom": true, - "isTestnet": true, - "name": "Atlas Testnet", - "slug": "atlas-testnet", - "parentChainId": 421614, - "retryableLifetimeSeconds": 604800, - "tokenBridge": { - "parentCustomGateway": "0xE5A8943e36f93491f97A521447772fCF529533B5", - "parentErc20Gateway": "0xBc89C0A2CCA8A0e1583AA6d4e22B36E26661C78E", - "parentGatewayRouter": "0xc93fDF8327e04776fd184188bB4EC8521E5E96D7", - "parentMultiCall": "0xce1CAd780c529e66e3aa6D952a1ED9A6447791c1", - "parentProxyAdmin": "0x0000000000000000000000000000000000000000", - "parentWeth": "0x0000000000000000000000000000000000000000", - "parentWethGateway": "0x0000000000000000000000000000000000000000", - "childCustomGateway": "0x2c2Be954C9B79d9cA2CF145d9d6363F3c3784615", - "childErc20Gateway": "0xDfCD9f6154BEA2E98e8aE493f3Fec2E22De52D6a", - "childGatewayRouter": "0xBc63Ec54dA9Ee3B2b3D4cA5DE3622a3F85e0F219", - "childMultiCall": "0x54AFaf5Fd025A99708e73e52c317454bD52B1e77", - "childProxyAdmin": "0xE1C9a27C47bb9691bA5b4E35eb7617B39F6098B9", - "childWeth": "0x0000000000000000000000000000000000000000", - "childWethGateway": "0x0000000000000000000000000000000000000000" - }, - "bridgeUiConfig": { - "color": "#889B6F", - "network": { - "name": "Atlas Testnet", - "logo": "/images/AtlasLogo.png", - "description": "Aarc is the first modular layer for unified borderless dapps." - }, - "nativeTokenData": { - "name": "Aarc", - "symbol": "AARC", - "decimals": 18, - "logoUrl": "/images/AtlasLogo.png" - } - } - }, { "chainId": 2730, "confirmPeriodBlocks": 64, @@ -781,6 +832,56 @@ "logoUrl": "/images/xrTokenLogo.png" } } + }, + { + "chainId": 631571, + "confirmPeriodBlocks": 900, + "ethBridge": { + "bridge": "0x133634FA8F372e59422744759a67796F01428BDD", + "inbox": "0x81Bf51dEC736adA7E256ed6C092CAe24D62a1ca7", + "outbox": "0x020e1cb41cF68999D988c20Ef40E2C58e1947db2", + "rollup": "0xAFcb6cF53AB01ac39577A592E141F614171c9371", + "sequencerInbox": "0x2453eb0d6F58C01Adb87505A4348fB5EEceB007D" + }, + "nativeToken": "0xe97f36a00058AA7DfC4E85d23532C3f70453a7aE", + "explorerUrl": "https://polter-testnet.explorer.alchemy.com", + "rpcUrl": "https://geist-polter.g.alchemy.com/public", + "isCustom": true, + "isTestnet": true, + "name": "Polter Testnet", + "slug": "polter-testnet", + "parentChainId": 84532, + "retryableLifetimeSeconds": 604800, + "tokenBridge": { + "parentCustomGateway": "0x24e1e6375f0f0Eecd38266357cADE57cA3A348F3", + "parentErc20Gateway": "0x9014E7244116b965F0D1059f80e2bD8169957117", + "parentGatewayRouter": "0xE38553a7989feD9e240DE8B0f5ed166BF75a088A", + "parentMultiCall": "0xE0753Df74d86D6B25aCd2d049389c7E52e2dd728", + "parentProxyAdmin": "0x0000000000000000000000000000000000000000", + "parentWeth": "0x0000000000000000000000000000000000000000", + "parentWethGateway": "0x0000000000000000000000000000000000000000", + "childCustomGateway": "0x7965162499751ABf89b8e3C640f8229AD8EeB022", + "childErc20Gateway": "0xA8E8b5D957e4ad9335E504442e877E513BD50729", + "childGatewayRouter": "0xF654086B1118fE98ea045De8D6dC0F1C53Da5B76", + "childMultiCall": "0xb97c5bC7DB7532471726550480A855484a408d00", + "childProxyAdmin": "0xda11A4ecaAcA51b1f2e2109a2EEFaC438409b68D", + "childWeth": "0x0000000000000000000000000000000000000000", + "childWethGateway": "0x0000000000000000000000000000000000000000" + }, + "bridgeUiConfig": { + "color": "#03AB2A", + "network": { + "name": "Polter Testnet", + "logo": "/images/PolterTestnetLogo.png", + "description": "A gaming testnet for Aavegotchi's Geist Mainnet." + }, + "nativeTokenData": { + "name": "Aavegotchi GHST Token", + "symbol": "GHST", + "decimals": 18, + "logoUrl": "/images/ghst.png" + } + } } ] } diff --git a/packages/arb-token-bridge-ui/src/util/wagmi/getWagmiChain.ts b/packages/arb-token-bridge-ui/src/util/wagmi/getWagmiChain.ts index bd6774c656..66de1b1b0f 100644 --- a/packages/arb-token-bridge-ui/src/util/wagmi/getWagmiChain.ts +++ b/packages/arb-token-bridge-ui/src/util/wagmi/getWagmiChain.ts @@ -9,7 +9,9 @@ import { arbitrumSepolia, localL1Network, localL2Network, - localL3Network + localL3Network, + baseSepolia, + base } from './wagmiAdditionalNetworks' import { ChainId } from '../networks' import { getCustomChainFromLocalStorageById } from '../networks' @@ -37,6 +39,9 @@ export function getWagmiChain(chainId: number): Chain { case ChainId.ArbitrumNova: return arbitrumNova + case ChainId.Base: + return base + // Testnets case ChainId.Sepolia: return sepolia @@ -47,6 +52,9 @@ export function getWagmiChain(chainId: number): Chain { case ChainId.ArbitrumSepolia: return arbitrumSepolia + case ChainId.BaseSepolia: + return baseSepolia + // Local networks case ChainId.Local: return localL1Network diff --git a/packages/arb-token-bridge-ui/src/util/wagmi/setup.ts b/packages/arb-token-bridge-ui/src/util/wagmi/setup.ts index 0023574b53..fac6d8c971 100644 --- a/packages/arb-token-bridge-ui/src/util/wagmi/setup.ts +++ b/packages/arb-token-bridge-ui/src/util/wagmi/setup.ts @@ -11,7 +11,9 @@ import { localL1Network as local, localL2Network as arbitrumLocal, localL3Network as l3Local, - holesky + holesky, + base, + baseSepolia } from './wagmiAdditionalNetworks' import { isTestingEnvironment } from '../CommonUtils' import { getCustomChainsFromLocalStorage, ChainId } from '../networks' @@ -26,16 +28,22 @@ const wagmiOrbitChains = getOrbitChains().map(chain => getWagmiChain(chain.chainId) ) +const defaultChains = [ + // mainnet, arb1, & arb nova are for network switch tests + mainnet, + arbitrum, + arbitrumNova, + base, + // sepolia & arb sepolia are for tx history panel tests + sepolia, + arbitrumSepolia, + baseSepolia, + holesky +] + const chainList = isTestingEnvironment ? [ - // mainnet, arb1, & arb nova are for network switch tests - mainnet, - arbitrum, - arbitrumNova, - // sepolia & arb sepolia are for tx history panel tests - sepolia, - arbitrumSepolia, - holesky, + ...defaultChains, // Orbit chains ...wagmiOrbitChains, // add local environments during testing @@ -45,16 +53,7 @@ const chainList = isTestingEnvironment // user-added custom chains ...customChains ] - : [ - mainnet, - arbitrum, - arbitrumNova, - sepolia, - arbitrumSepolia, - holesky, - ...wagmiOrbitChains, - ...customChains - ] + : [...defaultChains, ...wagmiOrbitChains, ...customChains] const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID! @@ -71,8 +70,10 @@ enum TargetChainKey { Ethereum = 'mainnet', ArbitrumOne = 'arbitrum-one', ArbitrumNova = 'arbitrum-nova', + Base = 'base', Sepolia = 'sepolia', - ArbitrumSepolia = 'arbitrum-sepolia' + ArbitrumSepolia = 'arbitrum-sepolia', + BaseSepolia = 'base-sepolia' } function sanitizeTargetChainKey(targetChainKey: string | null): TargetChainKey { @@ -100,11 +101,17 @@ function getChainId(targetChainKey: TargetChainKey): number { case TargetChainKey.ArbitrumNova: return ChainId.ArbitrumNova + case TargetChainKey.Base: + return ChainId.Base + case TargetChainKey.Sepolia: return ChainId.Sepolia case TargetChainKey.ArbitrumSepolia: return ChainId.ArbitrumSepolia + + case TargetChainKey.BaseSepolia: + return ChainId.BaseSepolia } } 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 4ab0f3b6a9..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, @@ -85,6 +95,28 @@ export const arbitrumSepolia: Chain = { } } +export const baseSepolia: Chain = { + id: ChainId.BaseSepolia, + name: 'Base Sepolia', + network: 'base-sepolia', + nativeCurrency: ether, + rpcUrls: { + default: { + http: [rpcURLs[ChainId.BaseSepolia]!] + }, + public: { + http: [rpcURLs[ChainId.BaseSepolia]!] + } + }, + blockExplorers: { + etherscan: { + name: 'Basescan', + url: explorerUrls[ChainId.BaseSepolia]! + }, + default: { name: 'Basescan', url: explorerUrls[ChainId.BaseSepolia]! } + } +} + export const arbitrumNova: Chain = { id: ChainId.ArbitrumNova, name: 'Arbitrum Nova', @@ -104,6 +136,25 @@ export const arbitrumNova: Chain = { } } +export const base: Chain = { + id: ChainId.Base, + name: 'Base', + network: 'base', + nativeCurrency: ether, + rpcUrls: { + default: { + http: [rpcURLs[ChainId.Base]!] + }, + public: { + http: [rpcURLs[ChainId.Base]!] + } + }, + blockExplorers: { + etherscan: { name: 'Basescan', url: explorerUrls[ChainId.Base]! }, + default: { name: 'Basescan', url: explorerUrls[ChainId.Base]! } + } +} + /** * For e2e testing */ 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) diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 0c53feef3e..e9d310eb63 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -17,6 +17,7 @@ "dependencies": { "@actions/core": "^1.10.1", "@actions/github": "^6.0.0", + "@arbitrum/sdk": "^4.0.1", "@octokit/rest": "^21.0.2", "axios": "^1.7.7", "commander": "^12.1.0", diff --git a/packages/scripts/src/addOrbitChain/index.ts b/packages/scripts/src/addOrbitChain/index.ts index 64d66647f0..13ce85c114 100644 --- a/packages/scripts/src/addOrbitChain/index.ts +++ b/packages/scripts/src/addOrbitChain/index.ts @@ -8,6 +8,7 @@ import { updateAndValidateOrbitChainsList, commitChangesAndCreatePR, setOutputs, + runPrettier, } from "./transforms"; /** @@ -36,6 +37,8 @@ export async function addOrbitChain(targetJsonPath: string): Promise { targetJsonPath ); + await runPrettier(targetJsonPath); + await commitChangesAndCreatePR( branchName, targetJsonPath, diff --git a/packages/scripts/src/addOrbitChain/schemas.ts b/packages/scripts/src/addOrbitChain/schemas.ts index 87ff3072de..34a3768518 100644 --- a/packages/scripts/src/addOrbitChain/schemas.ts +++ b/packages/scripts/src/addOrbitChain/schemas.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { constants, ethers } from "ethers"; import { getOctokit } from "@actions/github"; -export const TESTNET_PARENT_CHAIN_IDS = [11155111, 421614, 17000]; +export const TESTNET_PARENT_CHAIN_IDS = [11155111, 421614, 17000, 84532]; const ZERO_ADDRESS = constants.AddressZero; export const isValidAddress = (address: string): boolean => { @@ -128,6 +128,20 @@ export const chainSchema = z chainId: 17000, name: "Holesky", }; + case 8453: // Base + return { + rpcUrl: "https://mainnet.base.org", + blockExplorer: "https://basescan.io", + chainId: 8453, + name: "Base", + }; + case 84532: // Base Sepolia + return { + rpcUrl: "https://sepolia.base.org", + blockExplorer: "https://sepolia.basescan.io", + chainId: 84532, + name: "Base Sepolia", + }; default: throw new Error(`Unsupported parent chain ID: ${parentChainId}`); } diff --git a/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts b/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts index 564357146f..6b47b03366 100644 --- a/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts +++ b/packages/scripts/src/addOrbitChain/tests/__mocks__/chainDataMocks.ts @@ -180,7 +180,7 @@ export const mockIncomingChainData: IncomingChainData = { description: "This is a test chain.", chainLogo: "https://example.com/testchain.png", color: "#FF0000", - rpcUrl: "https://testrpc.com", + rpcUrl: "https://sepolia-rollup.arbitrum.io/rpc", explorerUrl: "https://testexplorer.com", parentChainId: "421614", confirmPeriodBlocks: "150", @@ -191,7 +191,7 @@ export const mockIncomingChainData: IncomingChainData = { bridge: "0x0000000000000000000000000000000000000001", inbox: "0x0000000000000000000000000000000000000002", outbox: "0x0000000000000000000000000000000000000003", - rollup: "0x0000000000000000000000000000000000000004", + rollup: "0xeedE9367Df91913ab149e828BDd6bE336df2c892", sequencerInbox: "0x0000000000000000000000000000000000000005", parentGatewayRouter: "0x0000000000000000000000000000000000000009", childGatewayRouter: "0x0000000000000000000000000000000000000016", diff --git a/packages/scripts/src/addOrbitChain/tests/__snapshots__/transforms.test.ts.snap b/packages/scripts/src/addOrbitChain/tests/__snapshots__/transforms.test.ts.snap index 70e1a3d876..84d87c2798 100644 --- a/packages/scripts/src/addOrbitChain/tests/__snapshots__/transforms.test.ts.snap +++ b/packages/scripts/src/addOrbitChain/tests/__snapshots__/transforms.test.ts.snap @@ -56,11 +56,11 @@ exports[`Transforms > transformIncomingDataToOrbitChain > should transform incom "chainId": 1234567890, "confirmPeriodBlocks": 150, "ethBridge": { - "bridge": "0x0000000000000000000000000000000000000001", - "inbox": "0x0000000000000000000000000000000000000002", - "outbox": "0x0000000000000000000000000000000000000003", - "rollup": "0x0000000000000000000000000000000000000004", - "sequencerInbox": "0x0000000000000000000000000000000000000005", + "bridge": "0x6c7FAC4edC72E86B3388B48979eF37Ecca5027e6", + "inbox": "0x6396825803B720bc6A43c63caa1DcD7B31EB4dd0", + "outbox": "0xc7491a559b416540427f9f112C5c98b1412c5d51", + "rollup": "0xeedE9367Df91913ab149e828BDd6bE336df2c892", + "sequencerInbox": "0x529a2061A1973be80D315770bA9469F3Da40D938", }, "explorerUrl": "https://testexplorer.com", "isArbitrum": true, @@ -70,7 +70,7 @@ exports[`Transforms > transformIncomingDataToOrbitChain > should transform incom "nativeToken": "0x0000000000000000000000000000000000000006", "parentChainId": 421614, "retryableLifetimeSeconds": 604800, - "rpcUrl": "https://testrpc.com", + "rpcUrl": "https://sepolia-rollup.arbitrum.io/rpc", "slug": "test-chain", "tokenBridge": { "childCustomGateway": "0x0000000000000000000000000000000000000014", diff --git a/packages/scripts/src/addOrbitChain/tests/transforms.test.ts b/packages/scripts/src/addOrbitChain/tests/transforms.test.ts index c7f0690106..d0c078f157 100644 --- a/packages/scripts/src/addOrbitChain/tests/transforms.test.ts +++ b/packages/scripts/src/addOrbitChain/tests/transforms.test.ts @@ -27,11 +27,11 @@ describe("Transforms", () => { }); describe("transformIncomingDataToOrbitChain", () => { - it("should transform incoming chain data to OrbitChain format", () => { + it("should transform incoming chain data to OrbitChain format", async () => { const chainLogoPath = "/images/mockChain_Logo.png"; const nativeTokenLogoPath = "/images/mockChain_NativeTokenLogo.png"; - const result = transformIncomingDataToOrbitChain( + const result = await transformIncomingDataToOrbitChain( mockIncomingChainData as IncomingChainData, chainLogoPath, nativeTokenLogoPath diff --git a/packages/scripts/src/addOrbitChain/transforms.ts b/packages/scripts/src/addOrbitChain/transforms.ts index e34dbf45a2..4a8c499a97 100644 --- a/packages/scripts/src/addOrbitChain/transforms.ts +++ b/packages/scripts/src/addOrbitChain/transforms.ts @@ -3,9 +3,12 @@ import * as core from "@actions/core"; import { warning } from "@actions/core"; +import { getArbitrumNetworkInformationFromRollup } from "@arbitrum/sdk"; +import { JsonRpcProvider } from "@ethersproject/providers"; import axios from "axios"; import * as fs from "fs"; import sharp from "sharp"; +import prettier from "prettier"; import { commitChanges, @@ -118,7 +121,7 @@ export const createAndValidateOrbitChain = async ( ) => { core.startGroup("Orbit Chain Creation and Validation"); console.log("Creating OrbitChain object..."); - const orbitChain = transformIncomingDataToOrbitChain( + const orbitChain = await transformIncomingDataToOrbitChain( validatedIncomingData, chainLogoPath, nativeTokenLogoPath @@ -131,7 +134,7 @@ export const createAndValidateOrbitChain = async ( }; export const updateAndValidateOrbitChainsList = async ( - orbitChain: ReturnType, + orbitChain: OrbitChain, targetJsonPath: string ) => { core.startGroup("Orbit ChainsList Update and Validation"); @@ -153,7 +156,7 @@ export const commitChangesAndCreatePR = async ( branchName: string, targetJsonPath: string, updatedOrbitChainsList: ReturnType, - orbitChain: ReturnType + orbitChain: OrbitChain ) => { core.startGroup("Commit Changes and Create Pull Request"); console.log("Preparing to commit changes..."); @@ -181,7 +184,7 @@ export const commitChangesAndCreatePR = async ( export const setOutputs = ( branchName: string, - orbitChain: ReturnType, + orbitChain: OrbitChain, targetJsonPath: string ) => { core.startGroup("Set Outputs"); @@ -334,23 +337,28 @@ export const fetchAndSaveImage = async ( return `/${imageSavePath}`; }; -export const transformIncomingDataToOrbitChain = ( +export const transformIncomingDataToOrbitChain = async ( chainData: IncomingChainData, chainLogoPath: string, nativeTokenLogoPath?: string -): OrbitChain => { +): Promise => { const parentChainId = parseInt(chainData.parentChainId, 10); const isTestnet = TESTNET_PARENT_CHAIN_IDS.includes(parentChainId); + const provider = new JsonRpcProvider(chainData.rpcUrl); + const rollupData = await getArbitrumNetworkInformationFromRollup( + chainData.rollup, + provider + ); return { chainId: parseInt(chainData.chainId, 10), - confirmPeriodBlocks: parseInt(chainData.confirmPeriodBlocks, 10), + confirmPeriodBlocks: rollupData.confirmPeriodBlocks, ethBridge: { - bridge: chainData.bridge, - inbox: chainData.inbox, - outbox: chainData.outbox, + bridge: rollupData.ethBridge.bridge, + inbox: rollupData.ethBridge.inbox, + outbox: rollupData.ethBridge.outbox, rollup: chainData.rollup, - sequencerInbox: chainData.sequencerInbox, + sequencerInbox: rollupData.ethBridge.sequencerInbox, }, nativeToken: chainData.nativeTokenAddress, explorerUrl: chainData.explorerUrl, @@ -430,3 +438,18 @@ export const updateOrbitChainsFile = ( return orbitChains; }; + +export async function runPrettier(targetJsonPath: string): Promise { + try { + const fileContent = fs.readFileSync(targetJsonPath, "utf8"); + const prettierConfig = await prettier.resolveConfig(targetJsonPath); + const formattedContent = await prettier.format(fileContent, { + ...prettierConfig, + filepath: targetJsonPath, + }); + fs.writeFileSync(targetJsonPath, formattedContent); + console.log(`Prettier formatting applied to ${targetJsonPath}`); + } catch (error) { + warning(`Failed to run Prettier: ${error}`); + } +}