diff --git a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRow.tsx b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRow.tsx index f7bbc970df..515ba807c6 100644 --- a/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRow.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransactionHistory/TransactionsTableRow.tsx @@ -167,11 +167,27 @@ export function TransactionsTableRow({ return tx.status === 'Failure' }, [tx]) + const testId = useMemo(() => { + const type = isClaimableTx ? 'claimable' : 'deposit' + const id = `${type}-row-${tx.txId}-${tx.value}${tx.asset}` + + if (tx.value2) { + return `${id}-${tx.value2}${nativeCurrency.symbol}` + } + + return id + }, [ + isClaimableTx, + nativeCurrency.symbol, + tx.asset, + tx.txId, + tx.value, + tx.value2 + ]) + return (
-
+
Add {nativeCurrency.symbol}
@@ -185,6 +188,7 @@ export function SourceNetworkBox({ maxAmount={maxAmount2} isMaxAmount={isMaxAmount2} decimals={nativeCurrency.decimals} + aria-label="Amount2 input" />

You can transfer {nativeCurrency.symbol} in the same transaction diff --git a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx index 2052a78420..32e8d9b4fb 100644 --- a/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx +++ b/packages/arb-token-bridge-ui/src/components/TransferPanel/TransferPanelMainInput.tsx @@ -74,9 +74,11 @@ function MaxButton({ } function SourceChainTokenBalance({ - balanceOverride + balanceOverride, + symbolOverride }: { balanceOverride?: AmountInputOptions['balance'] + symbolOverride?: AmountInputOptions['symbol'] }) { const { app: { selectedToken } @@ -105,15 +107,18 @@ function SourceChainTokenBalance({ }) : null + const symbol = + symbolOverride ?? selectedToken?.symbol ?? nativeCurrency.symbol + if (formattedBalance) { return ( <> Balance: {formattedBalance} @@ -284,7 +289,10 @@ export const TransferPanelMainInput = React.memo(

- +
diff --git a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts index 976809afc0..9ef565c058 100644 --- a/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts +++ b/packages/arb-token-bridge-ui/src/hooks/TransferPanel/useIsBatchTransferSupported.ts @@ -1,5 +1,4 @@ import { useAppState } from '../../state' -import { isExperimentalFeatureEnabled } from '../../util' import { isTokenNativeUSDC } from '../../util/TokenUtils' import { useNetworks } from '../useNetworks' import { useNetworksRelationship } from '../useNetworksRelationship' @@ -11,9 +10,6 @@ export const useIsBatchTransferSupported = () => { app: { selectedToken } } = useAppState() - if (!isExperimentalFeatureEnabled('batch')) { - return false - } if (!selectedToken) { return false } diff --git a/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts b/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts index 39ecfd63b8..3c63a7ec3a 100644 --- a/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts +++ b/packages/arb-token-bridge-ui/src/util/TokenDepositUtils.ts @@ -13,7 +13,6 @@ import { addressIsSmartContract } from './AddressUtils' import { getChainIdFromProvider } from '../token-bridge-sdk/utils' import { captureSentryErrorWithExtraData } from './SentryUtils' import { MergedTransaction } from '../state/app/state' -import { isExperimentalFeatureEnabled } from '.' async function fetchTokenFallbackGasEstimates({ inboxAddress, @@ -221,7 +220,6 @@ async function addressIsCustomGatewayToken({ export function isBatchTransfer(tx: MergedTransaction) { return ( - isExperimentalFeatureEnabled('batch') && !tx.isCctp && !tx.isWithdrawal && tx.assetType === AssetType.ERC20 && diff --git a/packages/arb-token-bridge-ui/src/util/index.ts b/packages/arb-token-bridge-ui/src/util/index.ts index 8392214fa0..4e96205668 100644 --- a/packages/arb-token-bridge-ui/src/util/index.ts +++ b/packages/arb-token-bridge-ui/src/util/index.ts @@ -54,7 +54,8 @@ export const getAPIBaseUrl = () => { return process.env.NODE_ENV === 'test' ? 'http://localhost:3000' : '' } -const featureFlags = ['batch'] as const +// add feature flags to the array +const featureFlags = [] as const type FeatureFlag = (typeof featureFlags)[number] 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 c3ce4b3cc3..063c7ca76e 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/cypress.d.ts @@ -8,7 +8,9 @@ import { searchAndSelectToken, fillCustomDestinationAddress, typeAmount, + typeAmount2, findAmountInput, + findAmount2Input, findSourceChainButton, findDestinationChainButton, findGasFeeSummary, @@ -53,7 +55,9 @@ declare global { }): typeof searchAndSelectToken fillCustomDestinationAddress(): typeof fillCustomDestinationAddress typeAmount: typeof typeAmount + typeAmount2: typeof typeAmount2 findAmountInput: typeof findAmountInput + findAmount2Input: typeof findAmount2Input findSourceChainButton: typeof findSourceChainButton findDestinationChainButton: typeof findDestinationChainButton findGasFeeForChain: typeof findGasFeeForChain diff --git a/packages/arb-token-bridge-ui/tests/e2e/specfiles.json b/packages/arb-token-bridge-ui/tests/e2e/specfiles.json index 7ca5456726..8e740d8962 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specfiles.json +++ b/packages/arb-token-bridge-ui/tests/e2e/specfiles.json @@ -24,6 +24,11 @@ "file": "tests/e2e/specs/**/withdrawERC20.cy.{js,jsx,ts,tsx}", "recordVideo": "false" }, + { + "name": "Batch deposit", + "file": "tests/e2e/specs/**/batchDeposit.cy.{js,jsx,ts,tsx}", + "recordVideo": "false" + }, { "name": "TX history", "file": "tests/e2e/specs/**/txHistory.cy.{js,jsx,ts,tsx}", 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 new file mode 100644 index 0000000000..08f768bc31 --- /dev/null +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/batchDeposit.cy.ts @@ -0,0 +1,273 @@ +import { + ERC20TokenSymbol, + getInitialERC20Balance, + getInitialETHBalance, + getL1NetworkConfig, + getL1NetworkName, + getL2NetworkConfig, + getL2NetworkName, + zeroToLessThanOneETH +} from '../../support/common' +import { formatAmount } from '../../../src/util/NumberUtils' + +describe('Batch Deposit', () => { + let parentNativeTokenBalance, + parentErc20Balance, + childNativeTokenBalance, + childErc20Balance: string + + beforeEach(() => { + getInitialERC20Balance({ + tokenAddress: Cypress.env('ERC20_TOKEN_ADDRESS_CHILD_CHAIN'), + multiCallerAddress: getL2NetworkConfig().multiCall, + address: Cypress.env('ADDRESS'), + rpcURL: Cypress.env('ARB_RPC_URL') + }).then(val => (childErc20Balance = formatAmount(val))) + + getInitialETHBalance( + Cypress.env('ARB_RPC_URL'), + Cypress.env('ADDRESS') + ).then(val => (childNativeTokenBalance = formatAmount(val))) + + getInitialETHBalance( + Cypress.env('ETH_RPC_URL'), + Cypress.env('ADDRESS') + ).then(val => (parentNativeTokenBalance = formatAmount(val))) + + getInitialERC20Balance({ + tokenAddress: Cypress.env('ERC20_TOKEN_ADDRESS_PARENT_CHAIN'), + multiCallerAddress: getL1NetworkConfig().multiCall, + address: Cypress.env('ADDRESS'), + rpcURL: Cypress.env('ETH_RPC_URL') + }).then(val => (parentErc20Balance = formatAmount(val))) + }) + + it('should show L1 and L2 chains, and ETH correctly', () => { + cy.login({ + networkType: 'parentChain', + url: '/' + }) + cy.findSourceChainButton(getL1NetworkName()) + cy.findDestinationChainButton(getL2NetworkName()) + cy.findSelectTokenButton('ETH') + }) + + it('should deposit erc-20 and native currency to the same address', () => { + // randomize the amount to be sure that previous transactions are not checked in e2e + const ERC20AmountToSend = Number((Math.random() * 0.001).toFixed(5)) + const nativeCurrencyAmountToSend = 0.002 + + const isOrbitTest = Cypress.env('ORBIT_TEST') == '1' + const depositTime = isOrbitTest ? 'Less than a minute' : '9 minutes' + + cy.login({ networkType: 'parentChain' }) + context('should add a new token', () => { + cy.searchAndSelectToken({ + tokenName: ERC20TokenSymbol, + tokenAddress: Cypress.env('ERC20_TOKEN_ADDRESS_PARENT_CHAIN') + }) + }) + + context('should show erc-20 parent balance correctly', () => { + cy.findByLabelText(`${ERC20TokenSymbol} balance amount on parentChain`) + .should('be.visible') + .contains(parentErc20Balance) + }) + + context('should show erc-20 child balance correctly', () => { + cy.findByLabelText(`${ERC20TokenSymbol} balance amount on childChain`) + .should('be.visible') + .contains(childErc20Balance) + }) + + context('native currency balance on child chain should not exist', () => { + cy.findByLabelText(`ETH balance amount on childChain`).should('not.exist') + }) + + context('amount2 input should not exist', () => { + cy.findAmount2Input().should('not.exist') + }) + + context('should click add native currency button', () => { + cy.findByLabelText('Add native currency button') + .should('be.visible') + .click() + }) + + context('amount2 input should show', () => { + cy.findAmount2Input().should('be.visible').should('have.value', '') + }) + + context('native currency balance on child chain should show', () => { + cy.findByLabelText(`ETH balance amount on childChain`) + .should('be.visible') + .contains(childNativeTokenBalance) + }) + + context('move funds button should be disabled', () => { + cy.findMoveFundsButton().should('be.disabled') + }) + + context('should show gas estimations and summary', () => { + cy.typeAmount(ERC20AmountToSend) + cy.typeAmount2(nativeCurrencyAmountToSend) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + }) + + const txData = { + symbol: ERC20TokenSymbol, + symbol2: 'ETH', + amount: ERC20AmountToSend, + amount2: nativeCurrencyAmountToSend + } + + context('should deposit successfully', () => { + cy.findMoveFundsButton().click() + cy.confirmMetamaskTransaction() + cy.findTransactionInTransactionHistory({ + ...txData, + duration: depositTime + }) + }) + + context('deposit should complete successfully', () => { + cy.selectTransactionsPanelTab('settled') + + cy.waitUntil(() => cy.findTransactionInTransactionHistory(txData), { + errorMsg: 'Could not find settled ERC20 Batch Deposit transaction', + timeout: 60_000, + interval: 500 + }) + + cy.findTransactionInTransactionHistory({ + duration: 'a few seconds ago', + ...txData + }) + cy.closeTransactionHistoryPanel() + }) + + context('funds should reach destination account successfully', () => { + // should have more funds on destination chain + cy.findByLabelText(`${ERC20TokenSymbol} balance amount on childChain`) + .invoke('text') + .then(parseFloat) + .should('be.gt', Number(parentErc20Balance)) + cy.findByLabelText(`ETH balance amount on childChain`) + .invoke('text') + .then(parseFloat) + .should( + 'be.gt', + Number(parentNativeTokenBalance) + nativeCurrencyAmountToSend + ) + + // the balance on the source chain should not be the same as before + cy.findByLabelText(`${ERC20TokenSymbol} balance amount on parentChain`) + .invoke('text') + .then(parseFloat) + .should('be.lt', Number(parentErc20Balance)) + }) + + context('transfer panel amount should be reset', () => { + cy.findAmountInput().should('have.value', '') + cy.findAmount2Input().should('have.value', '') + cy.findMoveFundsButton().should('be.disabled') + }) + }) + + it('should deposit erc-20 and native currency to a different address', () => { + // randomize the amount to be sure that previous transactions are not checked in e2e + const ERC20AmountToSend = Number((Math.random() * 0.001).toFixed(5)) + const nativeCurrencyAmountToSend = 0.002 + + const isOrbitTest = Cypress.env('ORBIT_TEST') == '1' + const depositTime = isOrbitTest ? 'Less than a minute' : '9 minutes' + + cy.login({ networkType: 'parentChain' }) + context('should add a new token', () => { + cy.searchAndSelectToken({ + tokenName: ERC20TokenSymbol, + tokenAddress: Cypress.env('ERC20_TOKEN_ADDRESS_PARENT_CHAIN') + }) + }) + + context('should fill custom destination address successfully', () => { + cy.fillCustomDestinationAddress() + }) + + context('amount2 input should not exist', () => { + cy.findAmount2Input().should('not.exist') + }) + + context('should click add native currency button', () => { + cy.findByLabelText('Add native currency button') + .should('be.visible') + .click() + }) + + context('amount2 input should show', () => { + cy.findAmount2Input().should('be.visible').should('have.value', '') + }) + + context('move funds button should be disabled', () => { + cy.findMoveFundsButton().should('be.disabled') + }) + + context('should show gas estimations and summary', () => { + cy.typeAmount(ERC20AmountToSend) + cy.typeAmount2(nativeCurrencyAmountToSend) + cy.findGasFeeSummary(zeroToLessThanOneETH) + cy.findGasFeeForChain(getL1NetworkName(), zeroToLessThanOneETH) + cy.findGasFeeForChain(getL2NetworkName(), zeroToLessThanOneETH) + }) + + const txData = { + symbol: ERC20TokenSymbol, + symbol2: 'ETH', + amount: ERC20AmountToSend, + amount2: nativeCurrencyAmountToSend + } + + context('should deposit successfully', () => { + cy.findMoveFundsButton().click() + cy.confirmMetamaskTransaction() + cy.findTransactionInTransactionHistory({ + ...txData, + duration: depositTime + }) + cy.openTransactionDetails(txData) + cy.findTransactionDetailsCustomDestinationAddress( + Cypress.env('CUSTOM_DESTINATION_ADDRESS') + ) + cy.closeTransactionDetails() + }) + + context('deposit should complete successfully', () => { + cy.selectTransactionsPanelTab('settled') + + cy.waitUntil(() => cy.findTransactionInTransactionHistory(txData), { + errorMsg: 'Could not find settled ERC20 Batch Deposit transaction', + timeout: 60_000, + interval: 500 + }) + + cy.findTransactionInTransactionHistory({ + duration: 'a few seconds ago', + ...txData + }) + cy.openTransactionDetails(txData) + cy.findTransactionDetailsCustomDestinationAddress( + Cypress.env('CUSTOM_DESTINATION_ADDRESS') + ) + cy.closeTransactionDetails() + cy.closeTransactionHistoryPanel() + }) + + context('transfer panel amount should be reset', () => { + cy.findAmountInput().should('have.value', '') + cy.findAmount2Input().should('have.value', '') + cy.findMoveFundsButton().should('be.disabled') + }) + }) +}) 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 9a6a2ed801..2568bfbef9 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 @@ -7,13 +7,12 @@ import { getInitialERC20Balance, getL1NetworkConfig, zeroToLessThanOneETH, + moreThanZeroBalance, getL1NetworkName, getL2NetworkName, ERC20TokenSymbol } from '../../support/common' -const moreThanZeroBalance = /0(\.\d+)/ - const depositTestCases = { 'Standard ERC20': { symbol: ERC20TokenSymbol, @@ -73,7 +72,6 @@ describe('Deposit Token', () => { cy.findByLabelText(`${testCase.symbol} balance amount on parentChain`) .should('be.visible') .contains(l1ERC20bal) - .should('be.visible') }) context('should show gas estimations', () => { diff --git a/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts b/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts index 4b19a7d3f5..7aff7357bc 100644 --- a/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts +++ b/packages/arb-token-bridge-ui/tests/e2e/specs/urlQueryParam.cy.ts @@ -251,3 +251,5 @@ describe('User enters site with query params on URL', () => { ) }) }) + +// TODO: Test amount2 when query params e2e is added back diff --git a/packages/arb-token-bridge-ui/tests/support/commands.ts b/packages/arb-token-bridge-ui/tests/support/commands.ts index b636e7ac86..53eeff9028 100644 --- a/packages/arb-token-bridge-ui/tests/support/commands.ts +++ b/packages/arb-token-bridge-ui/tests/support/commands.ts @@ -176,10 +176,20 @@ export function findAmountInput(): Cypress.Chainable> { return cy.findByLabelText('Amount input') } +export function findAmount2Input(): Cypress.Chainable> { + return cy.findByLabelText('Amount2 input') +} + export function typeAmount( amount: string | number ): Cypress.Chainable> { - return cy.findAmountInput().type(String(amount)) + return cy.findAmountInput().scrollIntoView().type(String(amount)) +} + +export function typeAmount2( + amount: string | number +): Cypress.Chainable> { + return cy.findAmount2Input().scrollIntoView().type(String(amount)) } export function findSourceChainButton( @@ -250,12 +260,21 @@ export function closeTransactionHistoryPanel() { export function openTransactionDetails({ amount, - symbol + amount2, + symbol, + symbol2 }: { amount: number + amount2?: number symbol: string + symbol2: string }): Cypress.Chainable> { - cy.findTransactionInTransactionHistory({ amount, symbol }).within(() => { + cy.findTransactionInTransactionHistory({ + amount, + amount2, + symbol, + symbol2 + }).within(() => { cy.findByLabelText('Transaction details button').click() }) return cy.findByText('Transaction details').should('be.visible') @@ -278,18 +297,26 @@ export function findTransactionDetailsCustomDestinationAddress( export function findTransactionInTransactionHistory({ symbol, + symbol2, amount, + amount2, duration }: { symbol: string + symbol2?: string amount: number + amount2?: number duration?: string }) { // Replace . with \. const parsedAmount = amount.toString().replace(/\./g, '\\.') + const rowId = new RegExp( - `(claimable|deposit)-row-[0-9xabcdef]*-${parsedAmount}${symbol}` + `(claimable|deposit)-row-[0-9xabcdef]*-${parsedAmount}${symbol}${ + amount2 && symbol2 ? `-${amount2}${symbol2}` : '' + }` ) + cy.findByTestId(rowId).as('row') if (duration) { cy.get('@row').findAllByText(duration).first().should('be.visible') @@ -336,7 +363,9 @@ Cypress.Commands.addAll({ searchAndSelectToken, fillCustomDestinationAddress, typeAmount, + typeAmount2, findAmountInput, + findAmount2Input, findSourceChainButton, findDestinationChainButton, findGasFeeForChain, diff --git a/packages/arb-token-bridge-ui/tests/support/common.ts b/packages/arb-token-bridge-ui/tests/support/common.ts index b1cc0011b1..575bba6e4d 100644 --- a/packages/arb-token-bridge-ui/tests/support/common.ts +++ b/packages/arb-token-bridge-ui/tests/support/common.ts @@ -95,6 +95,7 @@ export const ERC20TokenDecimals = 18 export const invalidTokenAddress = '0x0000000000000000000000000000000000000000' export const zeroToLessThanOneETH = /0(\.\d+)*( ETH)/ +export const moreThanZeroBalance = /0(\.\d+)/ export const importTokenThroughUI = (address: string) => { // Click on the ETH dropdown (Select token button)