diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 18c1e0c855..00e68cc1d5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -16,6 +16,7 @@ env: jobs: load-e2e-files: name: "Load e2e files" + if: inputs.test_type.type != 'cctp' runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.e2eFiles }} diff --git a/.github/workflows/formatSpecfiles.js b/.github/workflows/formatSpecfiles.js index cb7e382bbd..fac4b2f6c2 100644 --- a/.github/workflows/formatSpecfiles.js +++ b/.github/workflows/formatSpecfiles.js @@ -20,12 +20,13 @@ switch (testType) { break; } case "cctp": { - cctpFiles.forEach((spec) => { - tests.push({ - ...spec, - type: 'cctp', - }) - }) + // Running CCTP tests in parallel cause nonce issues, we're running the two tests sequentially + tests.push({ + name: "cctp", + file: "tests/e2e/specs/**/*Cctp.cy.{js,jsx,ts,tsx}", + recordVideo: false, + type: "cctp", + }); break; } } diff --git a/packages/arb-token-bridge-ui/synpress.cctp.config.ts b/packages/arb-token-bridge-ui/synpress.cctp.config.ts index edaa1ffd27..bc6679e52f 100644 --- a/packages/arb-token-bridge-ui/synpress.cctp.config.ts +++ b/packages/arb-token-bridge-ui/synpress.cctp.config.ts @@ -1,4 +1,4 @@ -import { BigNumber, Wallet, utils } from 'ethers' +import { BigNumber, Contract, Wallet, utils } from 'ethers' import { defineConfig } from 'cypress' import { Provider, StaticJsonRpcProvider } from '@ethersproject/providers' import synpressPlugins from '@synthetixio/synpress/plugins' @@ -13,6 +13,9 @@ import { import specFiles from './tests/e2e/cctp.json' import { CommonAddress } from './src/util/CommonAddressUtils' import { ERC20__factory } from '@arbitrum/sdk/dist/lib/abi/factories/ERC20__factory' +import { TokenMessengerAbi } from './src/util/cctp/TokenMessengerAbi' +import { ChainDomain } from './src/pages/api/cctp/[type]' +import { Address } from 'wagmi' export async function fundUsdc({ address, // wallet address where funding is required @@ -42,9 +45,11 @@ export async function fundUsdc({ const shouldRecordVideo = process.env.CYPRESS_RECORD_VIDEO === 'true' -const tests = process.env.TEST_FILE - ? [process.env.TEST_FILE] - : specFiles.map(file => file.file) +const tests = + process.env.TEST_FILE && + specFiles.find(file => file.name === process.env.TEST_FILE) + ? [process.env.TEST_FILE] + : specFiles.map(file => file.file) const INFURA_KEY = process.env.NEXT_PUBLIC_INFURA_KEY if (typeof INFURA_KEY === 'undefined') { @@ -76,40 +81,44 @@ async function fundWallets() { const userWalletAddress = userWallet.address console.log(`Funding wallet ${userWalletAddress}`) - const fundEthHelper = (network: 'sepolia' | 'arbSepolia') => { - return () => - fundEth({ - address: userWalletAddress, - sourceWallet: localWallet, - ...(network === 'sepolia' - ? { - provider: sepoliaProvider, - amount: ethAmountSepolia, - networkType: 'parentChain' - } - : { - provider: arbSepoliaProvider, - amount: ethAmountArbSepolia, - networkType: 'childChain' - }) - }) + const fundEthHelper = ( + network: 'sepolia' | 'arbSepolia', + amount: BigNumber + ) => { + return fundEth({ + address: userWalletAddress, + sourceWallet: localWallet, + ...(network === 'sepolia' + ? { + provider: sepoliaProvider, + amount, + networkType: 'parentChain' + } + : { + provider: arbSepoliaProvider, + amount, + networkType: 'childChain' + }) + }) } - const fundUsdcHelper = (network: 'sepolia' | 'arbSepolia') => { - return () => - fundUsdc({ - address: userWalletAddress, - sourceWallet: localWallet, - amount: usdcAmount, - ...(network === 'sepolia' - ? { - provider: sepoliaProvider, - networkType: 'parentChain' - } - : { - provider: arbSepoliaProvider, - networkType: 'childChain' - }) - }) + const fundUsdcHelper = ( + network: 'sepolia' | 'arbSepolia', + amount: BigNumber = usdcAmount + ) => { + return fundUsdc({ + address: userWalletAddress, + sourceWallet: localWallet, + amount, + ...(network === 'sepolia' + ? { + provider: sepoliaProvider, + networkType: 'parentChain' + } + : { + provider: arbSepoliaProvider, + networkType: 'childChain' + }) + }) } /** @@ -119,21 +128,74 @@ async function fundWallets() { const usdcAmount = utils.parseUnits('0.00063', 6) const ethAmountSepolia = utils.parseEther('0.025') const ethAmountArbSepolia = utils.parseEther('0.006') - const ethPromises: (() => Promise)[] = [] - const usdcPromises: (() => Promise)[] = [] if (tests.some(testFile => testFile.includes('deposit'))) { - ethPromises.push(fundEthHelper('sepolia')) - usdcPromises.push(fundUsdcHelper('sepolia')) + // Add ETH and USDC on ArbSepolia, to generate tx on ArbSepolia + await Promise.all([ + fundEthHelper('sepolia', ethAmountSepolia), + fundEthHelper('arbSepolia', utils.parseEther('0.01')) + ]) + await Promise.all([ + fundUsdcHelper('sepolia'), + fundUsdcHelper('arbSepolia', utils.parseUnits('0.00029', 6)) + ]) } if (tests.some(testFile => testFile.includes('withdraw'))) { - ethPromises.push(fundEthHelper('arbSepolia')) - usdcPromises.push(fundUsdcHelper('arbSepolia')) + // Add ETH and USDC on Sepolia, to generate tx on Sepolia + await Promise.all([ + fundEthHelper('arbSepolia', ethAmountArbSepolia), + fundEthHelper('sepolia', utils.parseEther('0.01')) + ]) + await Promise.all([ + fundUsdcHelper('arbSepolia'), + fundUsdcHelper('sepolia', utils.parseUnits('0.00025', 6)) + ]) } +} + +async function createCctpTx( + type: 'deposit' | 'withdrawal', + destinationAddress: Address, + amount: string +) { + console.log(`Creating CCTP transaction for ${destinationAddress}`) + const provider = type === 'deposit' ? sepoliaProvider : arbSepoliaProvider + const usdcAddress = + type === 'deposit' + ? CommonAddress.Sepolia.USDC + : CommonAddress.ArbitrumSepolia.USDC + const tokenMessengerContractAddress = + type === 'deposit' + ? CommonAddress.Sepolia.tokenMessengerContractAddress + : CommonAddress.ArbitrumSepolia.tokenMessengerContractAddress + + const signer = userWallet.connect(provider) + const usdcContract = ERC20__factory.connect(usdcAddress, signer) + + const tx = await usdcContract.functions.approve( + tokenMessengerContractAddress, + utils.parseUnits(amount, 6) + ) + + await tx.wait() + + const tokenMessenger = new Contract( + tokenMessengerContractAddress, + TokenMessengerAbi, + signer + ) + + await tokenMessenger.deployed() + + const depositForBurnTx = await tokenMessenger.functions.depositForBurn( + utils.parseUnits(amount, 6), + type === 'deposit' ? ChainDomain.ArbitrumOne : ChainDomain.Ethereum, + utils.hexlify(utils.zeroPad(destinationAddress, 32)), + usdcAddress + ) - await Promise.all(ethPromises.map(fn => fn())) - await Promise.all(usdcPromises.map(fn => fn())) + await depositForBurnTx.wait() } export default defineConfig({ @@ -144,12 +206,35 @@ export default defineConfig({ await fundWallets() + const customAddress = await getCustomDestinationAddress() config.env.PRIVATE_KEY = userWallet.privateKey config.env.PRIVATE_KEY_CCTP = process.env.PRIVATE_KEY_CCTP config.env.SEPOLIA_INFURA_RPC_URL = sepoliaRpcUrl config.env.ARB_SEPOLIA_INFURA_RPC_URL = arbSepoliaRpcUrl - config.env.CUSTOM_DESTINATION_ADDRESS = - await getCustomDestinationAddress() + config.env.CUSTOM_DESTINATION_ADDRESS = customAddress + + /** + * Currently, we can't confirm transaction on Sepolia, we need to programmatically deposit on Sepolia + * And claim on ArbSepolia + * + * - Create one deposit transaction, claimed in withdrawCctp + * - Create one deposit transaction to custom address, claimed in withdrawCctp + * - Create one withdraw transaction, rejected in depositCctp + * - Create one withdraw transaction to custom address, rejected in depositCctp + */ + if (tests.some(testFile => testFile.includes('deposit'))) { + await createCctpTx( + 'withdrawal', + userWallet.address as Address, + '0.00014' + ) + await createCctpTx('withdrawal', customAddress as Address, '0.00015') + } + + if (tests.some(testFile => testFile.includes('withdraw'))) { + await createCctpTx('deposit', userWallet.address as Address, '0.00012') + await createCctpTx('deposit', customAddress as Address, '0.00013') + } setupCypressTasks(on, { requiresNetworkSetup: false }) synpressPlugins(on, config) diff --git a/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts b/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts index 063c7ca76e..5b44d31925 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts @@ -24,7 +24,8 @@ import { findClaimButton, selectTransactionsPanelTab, confirmSpending, - closeTransactionHistoryPanel + closeTransactionHistoryPanel, + claimCctp } from '../support/commands' import { NetworkType, NetworkName } from '../support/common' @@ -71,6 +72,7 @@ declare global { findTransactionInTransactionHistory: typeof findTransactionInTransactionHistory findClaimButton: typeof findClaimButton confirmSpending: typeof confirmSpending + claimCctp: typeof claimCctp } } } 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 777ceafc74..f1d39bbf2d 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 @@ -4,6 +4,7 @@ import { zeroToLessThanOneETH } 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 = () => { @@ -104,6 +105,16 @@ describe('Deposit USDC through CCTP', () => { // timeout: 60_000 // } // }) + + // We have setup deposit transactions before running tests + cy.wait(40_000) + cy.rejectMetamaskTransaction() + }) + + it('should claim deposit', () => { + cy.claimCctp(0.00014, { accept: false }) + cy.closeTransactionHistoryPanel() + cy.claimCctp(0.00015, { accept: false }) }) /** diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts index 758f81a9c8..e1e062f1c4 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/withdrawCctp.cy.ts @@ -3,6 +3,7 @@ */ import { CommonAddress } from 'packages/arb-token-bridge-ui/src/util/CommonAddressUtils' +import { formatAmount } from 'packages/arb-token-bridge-ui/src/util/NumberUtils' // common function for this cctp withdrawal export const confirmAndApproveCctpWithdrawal = () => { @@ -77,11 +78,27 @@ describe('Withdraw USDC through CCTP', () => { cy.confirmSpending(USDCAmountToSend.toString()) // eslint-disable-next-line cy.wait(40_000) - cy.confirmMetamaskTransaction(undefined) + cy.confirmMetamaskTransaction({ gasConfig: 'aggressive' }) cy.findTransactionInTransactionHistory({ amount: USDCAmountToSend, symbol: 'USDC' }) + cy.findClaimButton( + formatAmount(USDCAmountToSend, { + symbol: 'USDC' + }), + { timeout: 120_000 } + ).click() + cy.allowMetamaskToSwitchNetwork() + cy.rejectMetamaskTransaction() + cy.changeMetamaskNetwork('arbitrum-sepolia') + }) + + it('should claim deposit', () => { + cy.changeMetamaskNetwork('sepolia') + cy.claimCctp(0.00012, { accept: true }) + cy.closeTransactionHistoryPanel() + cy.claimCctp(0.00013, { accept: true }) }) it('should initiate withdrawing USDC to custom destination address through CCTP successfully', () => { @@ -113,5 +130,14 @@ describe('Withdraw USDC through CCTP', () => { cy.findTransactionDetailsCustomDestinationAddress( Cypress.env('CUSTOM_DESTINATION_ADDRESS') ) + cy.closeTransactionDetails() + cy.findClaimButton( + formatAmount(USDCAmountToSend, { + symbol: 'USDC' + }), + { timeout: 120_000 } + ).click() + cy.allowMetamaskToSwitchNetwork() + cy.rejectMetamaskTransaction() }) }) diff --git a/packages/arb-token-bridge-ui/tests/support/commands.ts b/packages/arb-token-bridge-ui/tests/support/commands.ts index 53eeff9028..767668fb92 100644 --- a/packages/arb-token-bridge-ui/tests/support/commands.ts +++ b/packages/arb-token-bridge-ui/tests/support/commands.ts @@ -8,6 +8,7 @@ // *********************************************** import '@testing-library/cypress/add-commands' +import { SelectorMatcherOptions } from '@testing-library/cypress' import { NetworkType, NetworkName, @@ -16,6 +17,7 @@ import { getL2NetworkConfig } from './common' import { shortenAddress } from '../../src/util/CommonUtils' +import { formatAmount } from 'packages/arb-token-bridge-ui/src/util/NumberUtils' function shouldChangeNetwork(networkName: NetworkName) { // synpress throws if trying to connect to a network we are already connected to @@ -329,8 +331,13 @@ export function findTransactionInTransactionHistory({ } export function findClaimButton( - amountToClaim: string + amountToClaim: string, + options?: SelectorMatcherOptions ): Cypress.Chainable> { + if (options) { + return cy.findByLabelText(`Claim ${amountToClaim}`, options) + } + return cy.findByLabelText(`Claim ${amountToClaim}`) } @@ -354,6 +361,25 @@ export function confirmSpending( }) } +export function claimCctp(amount: number, options: { accept: boolean }) { + const formattedAmount = formatAmount(amount, { + symbol: 'USDC' + }) + cy.openTransactionsPanel('pending') + cy.findTransactionInTransactionHistory({ + amount, + symbol: 'USDC' + }) + cy.findClaimButton(formattedAmount, { timeout: 120_000 }).click() + if (options.accept) { + cy.confirmMetamaskTransaction(undefined) + cy.findByLabelText('show settled transactions').should('be.visible').click() + cy.findByText(formattedAmount).should('be.visible') + } else { + cy.rejectMetamaskTransaction() + } +} + Cypress.Commands.addAll({ connectToApp, login, @@ -378,5 +404,6 @@ Cypress.Commands.addAll({ findTransactionInTransactionHistory, findClaimButton, findTransactionDetailsCustomDestinationAddress, - confirmSpending + confirmSpending, + claimCctp })