import { act, fireEvent, render, waitFor, within } from '@testing-library/react-native' import BigNumber from 'bignumber.js' import React from 'react' import { DeviceEventEmitter } from 'react-native' import { getNumberFormatSettings } from 'react-native-localize' import { Provider } from 'react-redux' import AppAnalytics from 'src/analytics/AppAnalytics' import { EarnEvents, FeeEvents } from 'src/analytics/Events' import EarnEnterAmount from 'src/earn/EarnEnterAmount' import { usePrepareEnterAmountTransactionsCallback } from 'src/earn/hooks' import { Status as EarnStatus } from 'src/earn/slice' import { CICOFlow } from 'src/fiatExchanges/types' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { getFeatureGate, getMultichainFeatures } from 'src/statsig' import { StatsigFeatureGates } from 'src/statsig/types' import { SwapTransaction } from 'src/swap/types' import { TokenBalance } from 'src/tokens/slice' import { NetworkId } from 'src/transactions/types' import { PreparedTransactionsNeedDecreaseSpendAmountForGas, PreparedTransactionsNotEnoughBalanceForGas, PreparedTransactionsPossible, } from 'src/viem/prepareTransactions' import networkConfig from 'src/web3/networkConfig' import MockedNavigator from 'test/MockedNavigator' import { createMockStore } from 'test/utils' import { mockAaveArbUsdcTokenId, mockAccount, mockArbArbTokenId, mockArbEthTokenId, mockArbUsdcTokenId, mockCeloAddress, mockCeloTokenId, mockCusdTokenId, mockEarnPositions, mockPositions, mockRewardsPositions, mockTokenBalances, mockUSDCAddress, mockUSDCTokenId, } from 'test/values' jest.mock('src/earn/hooks') jest.mock('react-native-localize') jest.mock('src/statsig') // for cross chain swap and indirect use in hooksApiSelector jest.mocked(getMultichainFeatures).mockReturnValue({ showSwap: [ NetworkId['arbitrum-sepolia'], NetworkId['celo-alfajores'], NetworkId['ethereum-sepolia'], ], }) const mockPreparedTransaction: PreparedTransactionsPossible = { type: 'possible' as const, transactions: [ { from: '0xfrom', to: '0xto', data: '0xdata', gas: BigInt(5e12), _baseFeePerGas: BigInt(1), maxFeePerGas: BigInt(1), maxPriorityFeePerGas: undefined, }, { from: '0xfrom', to: '0xto', data: '0xdata', gas: BigInt(1e12), _baseFeePerGas: BigInt(1), maxFeePerGas: BigInt(1), maxPriorityFeePerGas: undefined, }, ], feeCurrency: { ...mockTokenBalances[mockArbEthTokenId], isNative: true, balance: new BigNumber(10), priceUsd: new BigNumber(1), lastKnownPriceUsd: new BigNumber(1), }, } const mockPreparedTransactionNotEnough: PreparedTransactionsNotEnoughBalanceForGas = { type: 'not-enough-balance-for-gas' as const, feeCurrencies: [ { ...mockTokenBalances[mockArbEthTokenId], isNative: true, balance: new BigNumber(0), priceUsd: new BigNumber(1500), lastKnownPriceUsd: new BigNumber(1500), }, ], } const mockPreparedTransactionDecreaseSpend: PreparedTransactionsNeedDecreaseSpendAmountForGas = { type: 'need-decrease-spend-amount-for-gas' as const, feeCurrency: { ...mockTokenBalances[mockArbEthTokenId], isNative: true, balance: new BigNumber(0), priceUsd: new BigNumber(1500), lastKnownPriceUsd: new BigNumber(1500), }, maxGasFeeInDecimal: new BigNumber(1), estimatedGasFeeInDecimal: new BigNumber(1), decreasedSpendAmount: new BigNumber(1), } const mockArbFeeCurrencies: TokenBalance[] = [ { ...mockTokenBalances[mockArbEthTokenId], isNative: true, balance: new BigNumber(1), priceUsd: new BigNumber(1500), lastKnownPriceUsd: new BigNumber(1500), }, ] const mockCeloFeeCurrencies: TokenBalance[] = [ { ...mockTokenBalances[mockCeloTokenId], isNative: true, balance: new BigNumber(5), priceUsd: new BigNumber(mockTokenBalances[mockCeloTokenId].priceUsd!), lastKnownPriceUsd: new BigNumber(mockTokenBalances[mockCeloTokenId].priceUsd!), }, { ...mockTokenBalances[mockCusdTokenId], balance: new BigNumber(5), priceUsd: new BigNumber(mockTokenBalances[mockCusdTokenId].priceUsd!), lastKnownPriceUsd: new BigNumber(mockTokenBalances[mockCusdTokenId].priceUsd!), }, ] const mockSwapTransaction: SwapTransaction = { swapType: 'same-chain', chainId: 42161, price: '2439', guaranteedPrice: '2377', appFeePercentageIncludedInPrice: '0.6', sellTokenAddress: '0xEeeeeeE', buyTokenAddress: mockUSDCAddress, sellAmount: '410000000000000', buyAmount: '1000000', allowanceTarget: '0x0000000000000000000000000000000000000123', from: mockAccount, to: '0x0000000000000000000000000000000000000123', value: '0', data: '0x0', gas: '1800000', estimatedGasUse: undefined, estimatedPriceImpact: '0.1', } const mockCrossChainSwapTransaction: SwapTransaction = { ...mockSwapTransaction, swapType: 'cross-chain', estimatedDuration: 300, maxCrossChainFee: '1000000000000000', estimatedCrossChainFee: '500000000000000', sellTokenAddress: mockCeloAddress, price: '4', guaranteedPrice: '4', } function createStore(depositStatus: EarnStatus = 'idle') { return createMockStore({ tokens: { tokenBalances: { ...mockTokenBalances, mockArbUsdcTokenId: { ...mockTokenBalances[mockArbUsdcTokenId], balance: '10', }, mockArbEthTokenId: { ...mockTokenBalances[mockArbEthTokenId], balance: '1', }, mockArbArbTokenId: { ...mockTokenBalances[mockArbArbTokenId], minimumAppVersionToSwap: '1.0.0', balance: '1', }, mockAaveArbUsdcTokenId: { ...mockTokenBalances[mockAaveArbUsdcTokenId], balance: '10', }, mockCeloTokenId: { ...mockTokenBalances[mockCeloTokenId], balance: '5', }, mockCusdTokenId: { ...mockTokenBalances[mockCusdTokenId], balance: '5', }, mockUSDCTokenId: { ...mockTokenBalances[mockUSDCTokenId], balance: '5', }, }, }, positions: { positions: [...mockPositions, ...mockRewardsPositions], }, earn: { depositStatus, }, }) } const store = createStore() const params = { pool: mockEarnPositions[0], } const mockPoolWithHighPricePerShare = { ...mockEarnPositions[0], pricePerShare: ['2'], balance: '10', } describe('EarnEnterAmount', () => { const refreshPreparedTransactionsSpy = jest.fn() beforeEach(() => { jest.clearAllMocks() jest .mocked(getFeatureGate) .mockImplementation( (featureGateName) => featureGateName === StatsigFeatureGates.SHOW_POSITIONS ) jest .mocked(getNumberFormatSettings) .mockReturnValue({ decimalSeparator: '.', groupingSeparator: ',' }) store.clearActions() jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: undefined, refreshPreparedTransactions: refreshPreparedTransactionsSpy, clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) }) describe('deposit', () => { const depositParams = { ...params, mode: 'deposit' } it('should show only the deposit token and not include the token dropdown', async () => { const { getByTestId, queryByTestId } = render( ) expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeTruthy() expect(getByTestId('EarnEnterAmount/TokenSelect')).toHaveTextContent('USDC') expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeDisabled() expect(queryByTestId('downArrowIcon')).toBeFalsy() }) it('should apply the maximum amount if the user selects the max option', async () => { const { getByTestId } = render( ) await act(() => { DeviceEventEmitter.emit('keyboardDidShow', { endCoordinates: { height: 100 } }) }) fireEvent.press(within(getByTestId('EarnEnterAmount/AmountOptions')).getByText('maxSymbol')) expect(getByTestId('EarnEnterAmount/TokenAmountInput').props.value).toBe('10') // balance }) it('should prepare transactions with the expected inputs', async () => { const { getByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '.25') await waitFor(() => expect(refreshPreparedTransactionsSpy).toHaveBeenCalledTimes(1)) expect(refreshPreparedTransactionsSpy).toHaveBeenCalledWith({ amount: '0.25', token: { ...mockTokenBalances[mockArbUsdcTokenId], priceUsd: new BigNumber(1), lastKnownPriceUsd: new BigNumber(1), balance: new BigNumber(10), }, walletAddress: mockAccount.toLowerCase(), pool: mockEarnPositions[0], hooksApiUrl: networkConfig.hooksApiUrl, feeCurrencies: mockArbFeeCurrencies, shortcutId: 'deposit', useMax: false, }) }) it('should show tx details and handle navigating to the deposit bottom sheet', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: undefined, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '8') await waitFor(() => expect(getByText('earnFlow.enterAmount.continue')).not.toBeDisabled()) expect(getByTestId('EarnEnterAmount/Deposit/Crypto')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Deposit/Crypto')).toHaveTextContent('8.00 USDC') expect(getByTestId('EarnEnterAmount/Deposit/Fiat')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Deposit/Fiat')).toHaveTextContent('₱10.64') expect(getByTestId('EarnEnterAmount/Fees')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Fees')).toHaveTextContent('₱0.012') fireEvent.press(getByText('earnFlow.enterAmount.continue')) await waitFor(() => expect(AppAnalytics.track).toHaveBeenCalledTimes(1)) expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_enter_amount_continue_press, { amountEnteredIn: 'token', amountInUsd: '8.00', networkId: NetworkId['arbitrum-sepolia'], depositTokenId: mockArbUsdcTokenId, providerId: mockEarnPositions[0].appId, poolId: mockEarnPositions[0].positionId, fromTokenId: mockArbUsdcTokenId, fromTokenAmount: '8', fromNetworkId: NetworkId['arbitrum-sepolia'], depositTokenAmount: '8', mode: 'deposit', }) await waitFor(() => expect(getByText('earnFlow.depositBottomSheet.title')).toBeVisible()) }) }) describe('swap-deposit', () => { const swapDepositParams = { ...params, mode: 'swap-deposit' } it('should show the token dropdown and allow the user to select a token only from same chain if feature gate is off', async () => { const { getByTestId, getAllByTestId } = render( ) expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeTruthy() expect(getByTestId('EarnEnterAmount/TokenSelect')).toHaveTextContent('ETH') expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeEnabled() expect(getByTestId('downArrowIcon')).toBeTruthy() expect(getAllByTestId('TokenBalanceItem')).toHaveLength(2) expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('ETH') expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('Arbitrum Sepolia') expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('ARB') expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('Arbitrum Sepolia') expect(getByTestId('TokenBottomSheet')).not.toHaveTextContent('USDC') }) it('should show the token dropdown and allow the user to select a token from all chains if feature gate is on', async () => { jest .mocked(getFeatureGate) .mockImplementation( (featureGateName) => featureGateName === StatsigFeatureGates.ALLOW_CROSS_CHAIN_SWAP_AND_DEPOSIT || featureGateName === StatsigFeatureGates.SHOW_POSITIONS ) const { getByTestId, getAllByTestId } = render( ) expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeTruthy() expect(getByTestId('EarnEnterAmount/TokenSelect')).toHaveTextContent('ETH') expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeEnabled() expect(getByTestId('downArrowIcon')).toBeTruthy() expect(getAllByTestId('TokenBalanceItem')).toHaveLength(6) expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('ETH') expect(getAllByTestId('TokenBalanceItem')[0]).toHaveTextContent('Arbitrum Sepolia') expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('ARB') expect(getAllByTestId('TokenBalanceItem')[1]).toHaveTextContent('Arbitrum Sepolia') expect(getAllByTestId('TokenBalanceItem')[2]).toHaveTextContent('CELO') expect(getAllByTestId('TokenBalanceItem')[2]).toHaveTextContent('Celo Alfajores') expect(getAllByTestId('TokenBalanceItem')[3]).toHaveTextContent('cUSD') expect(getAllByTestId('TokenBalanceItem')[3]).toHaveTextContent('Celo Alfajores') expect(getAllByTestId('TokenBalanceItem')[4]).toHaveTextContent('USDC') expect(getAllByTestId('TokenBalanceItem')[4]).toHaveTextContent('Ethereum Sepolia') expect(getAllByTestId('TokenBalanceItem')[5]).toHaveTextContent('POOF') expect(getAllByTestId('TokenBalanceItem')[5]).toHaveTextContent('Celo Alfajores') }) it('should default to the swappable token if only one is eligible and not show dropdown', async () => { const store = createMockStore({ tokens: { tokenBalances: { ...mockTokenBalances, [mockArbUsdcTokenId]: { ...mockTokenBalances[mockArbUsdcTokenId], balance: '10', }, mockArbEthTokenId: { ...mockTokenBalances[mockArbEthTokenId], minimumAppVersionToSwap: '1.0.0', balance: '0', // not eligible for swap }, mockArbArbTokenId: { ...mockTokenBalances[mockArbArbTokenId], balance: '1', // eligible for swap }, }, }, }) const { getByTestId, queryByTestId } = render( ) expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeTruthy() expect(getByTestId('EarnEnterAmount/TokenSelect')).toHaveTextContent('ARB') expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeDisabled() expect(queryByTestId('downArrowIcon')).toBeFalsy() }) it('should prepare transactions with the expected inputs for same-chain swap', async () => { const { getByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '.25') await waitFor(() => expect(refreshPreparedTransactionsSpy).toHaveBeenCalledTimes(1)) expect(refreshPreparedTransactionsSpy).toHaveBeenCalledWith({ amount: '0.25', token: { ...mockTokenBalances[mockArbEthTokenId], priceUsd: new BigNumber(1500), lastKnownPriceUsd: new BigNumber(1500), balance: new BigNumber(1), }, walletAddress: mockAccount.toLowerCase(), pool: mockEarnPositions[0], hooksApiUrl: networkConfig.hooksApiUrl, feeCurrencies: mockArbFeeCurrencies, shortcutId: 'swap-deposit', useMax: false, }) }) it('should prepare transactions with the expected inputs for cross-chain swap', async () => { jest .mocked(getFeatureGate) .mockImplementation( (featureGateName) => featureGateName === StatsigFeatureGates.ALLOW_CROSS_CHAIN_SWAP_AND_DEPOSIT || featureGateName === StatsigFeatureGates.SHOW_POSITIONS ) const { getByTestId, getAllByTestId } = render( ) fireEvent.press(getAllByTestId('TokenBalanceItem')[2]) // select celo for cross chain swap fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '.25') await waitFor(() => expect(refreshPreparedTransactionsSpy).toHaveBeenCalledTimes(1)) expect(refreshPreparedTransactionsSpy).toHaveBeenCalledWith({ amount: '0.25', token: { ...mockTokenBalances[mockCeloTokenId], priceUsd: new BigNumber(mockTokenBalances[mockCeloTokenId].priceUsd!), lastKnownPriceUsd: new BigNumber(mockTokenBalances[mockCeloTokenId].priceUsd!), balance: new BigNumber(5), }, walletAddress: mockAccount.toLowerCase(), pool: mockEarnPositions[0], hooksApiUrl: networkConfig.hooksApiUrl, feeCurrencies: expect.arrayContaining(mockCeloFeeCurrencies), shortcutId: 'swap-deposit', useMax: false, }) }) it('should show tx details and handle navigating to the deposit bottom sheet for same-chain swap', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: mockSwapTransaction, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText, queryByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '0.00041') await waitFor(() => expect(getByText('earnFlow.enterAmount.continue')).not.toBeDisabled()) expect(getByTestId('EarnEnterAmount/Swap/From')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Swap/From')).toHaveTextContent('0.00041 ETH') expect(getByTestId('EarnEnterAmount/Swap/To')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Swap/To')).toHaveTextContent('1.00 USDC') expect(getByTestId('EarnEnterAmount/Deposit/Crypto')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Deposit/Crypto')).toHaveTextContent('1.00 USDC') expect(getByTestId('EarnEnterAmount/Deposit/Fiat')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Deposit/Fiat')).toHaveTextContent('₱1.33') expect(getByTestId('EarnEnterAmount/Fees')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Fees')).toHaveTextContent('₱0.012') expect(queryByTestId('EarnEnterAmount/Duration')).toBeFalsy() fireEvent.press(getByText('earnFlow.enterAmount.continue')) await waitFor(() => expect(AppAnalytics.track).toHaveBeenCalledTimes(1)) expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_enter_amount_continue_press, { amountEnteredIn: 'token', amountInUsd: '0.62', networkId: NetworkId['arbitrum-sepolia'], fromTokenAmount: '0.00041', depositTokenId: mockArbUsdcTokenId, providerId: mockEarnPositions[0].appId, poolId: mockEarnPositions[0].positionId, fromTokenId: mockArbEthTokenId, fromNetworkId: NetworkId['arbitrum-sepolia'], depositTokenAmount: '0.99999', mode: 'swap-deposit', swapType: 'same-chain', }) await waitFor(() => expect(getByText('earnFlow.depositBottomSheet.title')).toBeVisible()) }) it('should show tx details and handle navigating to the deposit bottom sheet for cross-chain swap', async () => { jest .mocked(getFeatureGate) .mockImplementation( (featureGateName) => featureGateName === StatsigFeatureGates.ALLOW_CROSS_CHAIN_SWAP_AND_DEPOSIT || featureGateName === StatsigFeatureGates.SHOW_POSITIONS ) jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: mockCrossChainSwapTransaction, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText, getAllByTestId } = render( ) fireEvent.press(getAllByTestId('TokenBalanceItem')[2]) // select celo for cross chain swap fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '0.25') await waitFor(() => expect(getByText('earnFlow.enterAmount.continue')).not.toBeDisabled()) expect(getByTestId('EarnEnterAmount/Swap/From')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Swap/From')).toHaveTextContent('0.25 CELO') expect(getByTestId('EarnEnterAmount/Swap/To')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Swap/To')).toHaveTextContent('1.00 USDC') expect(getByTestId('EarnEnterAmount/Deposit/Crypto')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Deposit/Crypto')).toHaveTextContent('1.00 USDC') expect(getByTestId('EarnEnterAmount/Deposit/Fiat')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Deposit/Fiat')).toHaveTextContent('₱1.33') expect(getByTestId('EarnEnterAmount/Fees')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Fees')).toHaveTextContent('₱0.012') expect(getByTestId('EarnEnterAmount/Duration')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Duration')).toHaveTextContent('{"minutes":5}') fireEvent.press(getByText('earnFlow.enterAmount.continue')) await waitFor(() => expect(AppAnalytics.track).toHaveBeenCalledTimes(2)) // one for token selection, one for continue press expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_enter_amount_continue_press, { amountEnteredIn: 'token', amountInUsd: '3.31', networkId: NetworkId['arbitrum-sepolia'], fromTokenAmount: '0.25', depositTokenId: mockArbUsdcTokenId, providerId: mockEarnPositions[0].appId, poolId: mockEarnPositions[0].positionId, fromTokenId: mockCeloTokenId, fromNetworkId: NetworkId['celo-alfajores'], depositTokenAmount: '1', mode: 'swap-deposit', swapType: 'cross-chain', }) await waitFor(() => expect(getByText('earnFlow.depositBottomSheet.title')).toBeVisible()) }) }) describe('withdraw', () => { const withdrawParams = { ...params, mode: 'withdraw' } it('should show the deposit token and a disabled token dropdown', async () => { const { getByTestId, queryByTestId } = render( ) expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeTruthy() expect(getByTestId('EarnEnterAmount/TokenSelect')).toHaveTextContent('USDC') expect(getByTestId('EarnEnterAmount/TokenSelect')).toBeDisabled() expect(queryByTestId('downArrowIcon')).toBeFalsy() }) it('should apply the maximum amount if the user selects the max option', async () => { const { getByTestId } = render( ) await act(() => { DeviceEventEmitter.emit('keyboardDidShow', { endCoordinates: { height: 100 } }) }) fireEvent.press(within(getByTestId('EarnEnterAmount/AmountOptions')).getByText('maxSymbol')) expect(getByTestId('EarnEnterAmount/TokenAmountInput').props.value).toBe('11') // balance * pool price per share }) it('should prepare transactions with the expected inputs', async () => { const { getByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '.25') await waitFor(() => expect(refreshPreparedTransactionsSpy).toHaveBeenCalledTimes(1)) expect(refreshPreparedTransactionsSpy).toHaveBeenCalledWith({ amount: '0.125', token: { ...mockTokenBalances[mockAaveArbUsdcTokenId], priceUsd: new BigNumber(1), lastKnownPriceUsd: new BigNumber(1), balance: new BigNumber(10), }, walletAddress: mockAccount.toLowerCase(), pool: mockPoolWithHighPricePerShare, hooksApiUrl: networkConfig.hooksApiUrl, feeCurrencies: mockArbFeeCurrencies, shortcutId: 'withdraw', useMax: false, }) }) it('should show tx details for withdrawal', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: undefined, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '8') await waitFor(() => expect(getByText('earnFlow.enterAmount.continue')).not.toBeDisabled()) expect(getByTestId('EarnEnterAmount/Fees')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Fees')).toHaveTextContent('₱0.012') fireEvent.press(getByText('earnFlow.enterAmount.continue')) await waitFor(() => expect(AppAnalytics.track).toHaveBeenCalledTimes(1)) expect(AppAnalytics.track).toHaveBeenCalledWith(EarnEvents.earn_enter_amount_continue_press, { amountEnteredIn: 'token', amountInUsd: '8.00', networkId: NetworkId['arbitrum-sepolia'], depositTokenId: mockArbUsdcTokenId, providerId: mockEarnPositions[0].appId, poolId: mockEarnPositions[0].positionId, fromTokenId: 'arbitrum-sepolia:0x94a9d9ac8a22534e3faca9f4e7f2e2cf85d5e4c8', fromTokenAmount: '8', fromNetworkId: NetworkId['arbitrum-sepolia'], mode: 'withdraw', }) expect(navigate).toHaveBeenCalledWith(Screens.EarnConfirmationScreen, { pool: mockEarnPositions[0], mode: 'withdraw', inputAmount: '8', useMax: false, }) }) it('should allow the user to set an input value over the pool balance if pricePerShare is greater than 1', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: undefined, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, queryByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '15') expect(queryByTestId('EarnEnterAmount/NotEnoughBalanceWarning')).toBeFalsy() expect(getByTestId('EarnEnterAmount/Continue')).toBeEnabled() }) it('should not allow the user to set an input amount higher than pool balance * pricePerShare', async () => { const { getByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '20.001') expect(getByTestId('EarnEnterAmount/NotEnoughBalanceWarning')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Continue')).toBeDisabled() }) it('should show the Claiming Reward line item if withdrawalIncludesClaim is true and user has rewards', async () => { const { getByTestId } = render( ) expect(getByTestId('LabelWithInfo/ClaimingReward-0')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Reward-0')).toHaveTextContent('₱0.016') expect(getByTestId('EarnEnterAmount/Reward-0-crypto')).toHaveTextContent('0.01 ARB') }) it('should show the Withdrawing and Claiming card if withdrawalIncludesClaim is true', async () => { const { getByTestId } = render( ) expect(getByTestId('EarnEnterAmount/WithdrawingAndClaimingCard')).toBeTruthy() }) }) // tests independent of deposit / swap-deposit it('should show a warning and not allow the user to continue if they input an amount greater than balance', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: undefined, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '12') expect(getByTestId('EarnEnterAmount/NotEnoughBalanceWarning')).toBeTruthy() expect(getByTestId('EarnEnterAmount/Continue')).toBeDisabled() }) it('should show loading spinner when transaction submitted', async () => { const mockStore = createStore('loading') const { getByTestId } = render( ) await waitFor(() => expect(getByTestId('EarnEnterAmount/Continue')).toContainElement( getByTestId('Button/Loading') ) ) expect(getByTestId('EarnEnterAmount/Continue')).toBeDisabled() }) it('should show loading spinner when preparing transaction', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: undefined, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: true, }) const { getByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '8') await waitFor(() => expect(getByTestId('EarnEnterAmount/Continue')).toContainElement( getByTestId('Button/Loading') ) ) expect(getByTestId('EarnEnterAmount/Continue')).toBeDisabled() }) describe.each([ { decimal: '.', group: ',' }, { decimal: ',', group: '.' }, ])('with decimal separator "$decimal" and group separator "$group"', ({ decimal, group }) => { const replaceSeparators = (value: string) => value.replace(/\./g, '|').replace(/,/g, group).replace(/\|/g, decimal) const defaultFormat = BigNumber.config().FORMAT beforeEach(() => { jest .mocked(getNumberFormatSettings) .mockReturnValue({ decimalSeparator: decimal, groupingSeparator: group }) BigNumber.config({ FORMAT: { decimalSeparator: decimal, groupSeparator: group, groupSize: 3, }, }) }) afterEach(() => { BigNumber.config({ FORMAT: defaultFormat }) }) const mockStore = createMockStore({ tokens: { tokenBalances: { ...mockTokenBalances, [mockArbUsdcTokenId]: { ...mockTokenBalances[mockArbUsdcTokenId], balance: '100000.42', }, }, }, }) it('selecting max token amount applies correct decimal separator', async () => { const { getByTestId } = render( ) await act(() => { DeviceEventEmitter.emit('keyboardDidShow', { endCoordinates: { height: 100 } }) }) fireEvent.press(within(getByTestId('EarnEnterAmount/AmountOptions')).getByText('maxSymbol')) expect(getByTestId('EarnEnterAmount/TokenAmountInput').props.value).toBe( replaceSeparators('100,000.42') ) expect(getByTestId('EarnEnterAmount/ExchangeAmount')).toHaveTextContent( replaceSeparators('₱133,000.56') ) }) }) it('should show gas warning error when prepareTransactionsResult is type not-enough-balance-for-gas, and tapping cta behaves as expected', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransactionNotEnough, swapTransaction: undefined, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText } = render( ) await waitFor(() => expect(getByTestId('GasFeeWarning')).toBeTruthy()) fireEvent.press(getByText('gasFeeWarning.ctaBuy, {"tokenSymbol":"ETH"}')) expect(AppAnalytics.track).toHaveBeenCalledTimes(2) expect(AppAnalytics.track).toHaveBeenCalledWith(FeeEvents.gas_fee_warning_impression, { errorType: 'not-enough-balance-for-gas', flow: 'Deposit', tokenId: mockArbEthTokenId, networkId: NetworkId['arbitrum-sepolia'], }) expect(AppAnalytics.track).toHaveBeenCalledWith(FeeEvents.gas_fee_warning_cta_press, { errorType: 'not-enough-balance-for-gas', flow: 'Deposit', tokenId: mockArbEthTokenId, networkId: NetworkId['arbitrum-sepolia'], }) expect(navigate).toHaveBeenCalledWith(Screens.FiatExchangeAmount, { tokenId: mockArbEthTokenId, flow: CICOFlow.CashIn, tokenSymbol: 'ETH', }) }) it('should show gas warning error when prepareTransactionsResult is type need-decrease-spend-amount-for-gas, and tapping cta behaves as expected', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransactionDecreaseSpend, swapTransaction: undefined, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText } = render( ) await waitFor(() => expect(getByTestId('GasFeeWarning')).toBeTruthy()) fireEvent.press(getByText('gasFeeWarning.ctaAction, {"context":"Deposit"}')) expect(AppAnalytics.track).toHaveBeenCalledTimes(2) expect(AppAnalytics.track).toHaveBeenCalledWith(FeeEvents.gas_fee_warning_impression, { errorType: 'need-decrease-spend-amount-for-gas', flow: 'Deposit', tokenId: mockArbEthTokenId, networkId: NetworkId['arbitrum-sepolia'], }) expect(AppAnalytics.track).toHaveBeenCalledWith(FeeEvents.gas_fee_warning_cta_press, { errorType: 'need-decrease-spend-amount-for-gas', flow: 'Deposit', tokenId: mockArbEthTokenId, networkId: NetworkId['arbitrum-sepolia'], }) // Deposit value should now be decreasedSpendAmount from mockPreparedTransactionDecreaseSpend, which is 1 expect(getByTestId('EarnEnterAmount/Deposit/Crypto')).toHaveTextContent('1.00 USDC') }) it('should show the FeeDetailsBottomSheet when the user taps the fee details icon', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: undefined, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText, queryByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '1') fireEvent.press(getByTestId('LabelWithInfo/FeeLabel')) expect(getByText('earnFlow.enterAmount.feeBottomSheet.feeDetails')).toBeVisible() expect(getByTestId('EstNetworkFee')).toBeTruthy() expect(getByTestId('MaxNetworkFee')).toBeTruthy() expect(queryByTestId('SwapFee')).toBeFalsy() expect(queryByTestId('EstCrossChainFee')).toBeFalsy() expect(queryByTestId('MaxCrossChainFee')).toBeFalsy() expect(getByTestId('EstNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)') expect(getByTestId('MaxNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)') expect( getByText('earnFlow.enterAmount.feeBottomSheet.description, {"context":"deposit"}') ).toBeVisible() }) it('should show swap fees on the FeeDetailsBottomSheet when swap transaction is present', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: mockSwapTransaction, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText, queryByTestId } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '1') fireEvent.press(getByTestId('LabelWithInfo/FeeLabel')) expect(getByText('earnFlow.enterAmount.feeBottomSheet.feeDetails')).toBeVisible() expect(getByTestId('EstNetworkFee')).toBeTruthy() expect(getByTestId('MaxNetworkFee')).toBeTruthy() expect(getByTestId('SwapFee')).toBeTruthy() expect(queryByTestId('EstCrossChainFee')).toBeFalsy() expect(queryByTestId('MaxCrossChainFee')).toBeFalsy() expect(getByTestId('EstNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)') expect(getByTestId('MaxNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)') expect(getByTestId('SwapFee/Value')).toHaveTextContent('₱0.008 (0.006 USDC)') expect( getByText( 'earnFlow.enterAmount.feeBottomSheet.description, {"context":"depositSwapFee","appFeePercentage":"0.6"}' ) ).toBeVisible() expect(getByTestId('FeeDetailsBottomSheet/GotIt')).toBeVisible() }) it('should show swap and cross chain fees on the FeeDetailsBottomSheet when cross chain swap transaction is present', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: mockCrossChainSwapTransaction, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '1') fireEvent.press(getByTestId('LabelWithInfo/FeeLabel')) expect(getByText('earnFlow.enterAmount.feeBottomSheet.feeDetails')).toBeVisible() expect(getByTestId('EstNetworkFee')).toBeTruthy() expect(getByTestId('MaxNetworkFee')).toBeTruthy() expect(getByTestId('SwapFee')).toBeTruthy() expect(getByTestId('EstCrossChainFee')).toBeTruthy() expect(getByTestId('MaxCrossChainFee')).toBeTruthy() expect(getByTestId('EstNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)') expect(getByTestId('MaxNetworkFee/Value')).toHaveTextContent('₱0.012 (0.000006 ETH)') expect(getByTestId('SwapFee/Value')).toHaveTextContent('₱0.008 (0.006 USDC)') expect(getByTestId('EstCrossChainFee/Value')).toHaveTextContent('₱1.00 (0.0005 ETH)') expect(getByTestId('MaxCrossChainFee/Value')).toHaveTextContent('₱2.00 (0.001 ETH)') expect( getByText( 'earnFlow.enterAmount.feeBottomSheet.description, {"context":"depositCrossChainWithSwapFee","appFeePercentage":"0.6"}' ) ).toBeVisible() expect(getByTestId('FeeDetailsBottomSheet/GotIt')).toBeVisible() }) it('should display swap bottom sheet when the user taps the swap details icon', async () => { jest.mocked(usePrepareEnterAmountTransactionsCallback).mockReturnValue({ prepareTransactionsResult: { prepareTransactionsResult: mockPreparedTransaction, swapTransaction: mockSwapTransaction, }, refreshPreparedTransactions: jest.fn(), clearPreparedTransactions: jest.fn(), prepareTransactionError: undefined, isPreparingTransactions: false, }) const { getByTestId, getByText } = render( ) fireEvent.changeText(getByTestId('EarnEnterAmount/TokenAmountInput'), '1') fireEvent.press(getByTestId('LabelWithInfo/SwapLabel')) expect(getByText('earnFlow.enterAmount.swapBottomSheet.swapDetails')).toBeVisible() expect(getByTestId('SwapTo')).toBeTruthy() expect(getByTestId('SwapFrom')).toBeTruthy() expect(getByTestId('SwapTo/Value')).toBeTruthy() expect(getByTestId('SwapFrom/Value')).toBeTruthy() expect(getByText('earnFlow.enterAmount.swapBottomSheet.whySwap')).toBeVisible() expect(getByText('earnFlow.enterAmount.swapBottomSheet.swapDescription')).toBeVisible() expect(getByTestId('SwapDetailsBottomSheet/GotIt')).toBeVisible() }) })