import sinon from 'sinon'; import createMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import { ethers } from 'ethers'; import { CONTRACT_ADDRESS_ERROR, INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, INVALID_RECIPIENT_ADDRESS_ERROR, KNOWN_RECIPIENT_ADDRESS_WARNING, NEGATIVE_ETH_ERROR, } from '../../pages/send/send.constants'; import { MAINNET_CHAIN_ID, RINKEBY_CHAIN_ID, } from '../../../shared/constants/network'; import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas'; import { TRANSACTION_ENVELOPE_TYPES, TRANSACTION_TYPES, } from '../../../shared/constants/transaction'; import * as Actions from '../../store/actions'; import sendReducer, { initialState, initializeSendState, updateSendAmount, updateSendAsset, updateRecipientUserInput, useContactListForRecipientSearch, useMyAccountsForRecipientSearch, updateRecipient, resetRecipientInput, updateSendHexData, toggleSendMaxMode, signTransaction, SEND_STATUSES, ASSET_TYPES, SEND_STAGES, AMOUNT_MODES, RECIPIENT_SEARCH_MODES, editTransaction, getGasLimit, getGasPrice, getGasTotal, gasFeeIsInError, getMinimumGasLimitForSend, getGasInputMode, GAS_INPUT_MODES, getSendAsset, getSendAssetAddress, getIsAssetSendable, getSendAmount, getIsBalanceInsufficient, getSendMaxModeState, getDraftTransactionID, sendAmountIsInError, getSendHexData, getSendTo, getIsUsingMyAccountForRecipientSearch, getRecipientUserInput, getRecipient, getSendErrors, isSendStateInitialized, isSendFormInvalid, getSendStage, updateGasPrice, } from './send'; const mockStore = createMockStore([thunk]); jest.mock('./send', () => { const actual = jest.requireActual('./send'); return { __esModule: true, ...actual, getERC20Balance: jest.fn(() => '0x0'), }; }); describe('Send Slice', () => { let getTokenStandardAndDetailsStub; beforeEach(() => { getTokenStandardAndDetailsStub = jest .spyOn(Actions, 'getTokenStandardAndDetails') .mockImplementation(() => Promise.resolve({ standard: 'ERC20' })); jest .spyOn(Actions, 'estimateGas') .mockImplementation(() => Promise.resolve('0x0')); jest .spyOn(Actions, 'getGasFeeEstimatesAndStartPolling') .mockImplementation(() => Promise.resolve()); jest .spyOn(Actions, 'updateTokenType') .mockImplementation(() => Promise.resolve({ isERC721: false })); jest .spyOn(Actions, 'isCollectibleOwner') .mockImplementation(() => Promise.resolve(true)); }); describe('Reducers', () => { describe('updateSendAmount', () => { it('should', async () => { const action = { type: 'send/updateSendAmount', payload: '0x1' }; const result = sendReducer(initialState, action); expect(result.amount.value).toStrictEqual('0x1'); }); }); describe('updateAmountToMax', () => { it('should calculate the max amount based off of the asset balance and gas total then updates send amount value', () => { const maxAmountState = { amount: { value: '', }, asset: { balance: '0x56bc75e2d63100000', // 100000000000000000000 }, gas: { gasLimit: '0x5208', // 21000 gasTotal: '0x1319718a5000', // 21000000000000 minimumGasLimit: '0x5208', }, }; const state = { ...initialState, ...maxAmountState }; const action = { type: 'send/updateAmountToMax' }; const result = sendReducer(state, action); expect(result.amount.value).toStrictEqual('0x56bc74b13f185b000'); // 99999979000000000000 }); }); describe('updateGasFees', () => { it('should work with FEE_MARKET gas fees', () => { const action = { type: 'send/updateGasFees', payload: { transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, maxFeePerGas: '0x2', maxPriorityFeePerGas: '0x1', }, }; const result = sendReducer(initialState, action); expect(result.gas.maxFeePerGas).toStrictEqual( action.payload.maxFeePerGas, ); expect(result.gas.maxPriorityFeePerGas).toStrictEqual( action.payload.maxPriorityFeePerGas, ); expect(result.transactionType).toBe( TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, ); }); it('should work with LEGACY gas fees', () => { const action = { type: 'send/updateGasFees', payload: { transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY, gasPrice: '0x1', }, }; const result = sendReducer(initialState, action); expect(result.gas.gasPrice).toStrictEqual(action.payload.gasPrice); expect(result.transactionType).toBe(TRANSACTION_ENVELOPE_TYPES.LEGACY); }); }); describe('updateUserInputHexData', () => { it('should update the state with the provided data', () => { const action = { type: 'send/updateUserInputHexData', payload: 'TestData', }; const result = sendReducer(initialState, action); expect(result.draftTransaction.userInputHexData).toStrictEqual( action.payload, ); }); }); describe('updateGasLimit', () => { const action = { type: 'send/updateGasLimit', payload: '0x5208', // 21000 }; it('should', () => { const result = sendReducer( { ...initialState, stage: SEND_STAGES.DRAFT, gas: { ...initialState.gas, isGasEstimateLoading: false }, }, action, ); expect(result.gas.gasLimit).toStrictEqual(action.payload); expect(result.draftTransaction.txParams.gas).toStrictEqual( action.payload, ); }); it('should recalculate gasTotal', () => { const gasState = { ...initialState, gas: { gasLimit: '0x0', gasPrice: '0x3b9aca00', // 1000000000 }, }; const result = sendReducer(gasState, action); expect(result.gas.gasLimit).toStrictEqual(action.payload); expect(result.gas.gasPrice).toStrictEqual(gasState.gas.gasPrice); expect(result.gas.gasTotal).toStrictEqual('0x1319718a5000'); // 21000000000000 }); }); describe('updateAmountMode', () => { it('should change to INPUT amount mode', () => { const emptyAmountModeState = { amount: { mode: '', }, }; const action = { type: 'send/updateAmountMode', payload: AMOUNT_MODES.INPUT, }; const result = sendReducer(emptyAmountModeState, action); expect(result.amount.mode).toStrictEqual(action.payload); }); it('should change to MAX amount mode', () => { const action = { type: 'send/updateAmountMode', payload: AMOUNT_MODES.MAX, }; const result = sendReducer(initialState, action); expect(result.amount.mode).toStrictEqual(action.payload); }); it('should', () => { const action = { type: 'send/updateAmountMode', payload: 'RANDOM', }; const result = sendReducer(initialState, action); expect(result.amount.mode).not.toStrictEqual(action.payload); }); }); describe('updateAsset', () => { it('should update asset type and balance from respective action payload', () => { const updateAssetState = { ...initialState, asset: { type: 'old type', balance: 'old balance', }, }; const action = { type: 'send/updateAsset', payload: { type: 'new type', balance: 'new balance', }, }; const result = sendReducer(updateAssetState, action); expect(result.asset.type).toStrictEqual(action.payload.type); expect(result.asset.balance).toStrictEqual(action.payload.balance); }); it('should nullify old contract address error when asset types is not TOKEN', () => { const recipientErrorState = { ...initialState, recipient: { error: CONTRACT_ADDRESS_ERROR, }, asset: { type: ASSET_TYPES.TOKEN, }, }; const action = { type: 'send/updateAsset', payload: { type: 'New Type', }, }; const result = sendReducer(recipientErrorState, action); expect(result.recipient.error).not.toStrictEqual( recipientErrorState.recipient.error, ); expect(result.recipient.error).toBeNull(); }); it('should nullify old known address error when asset types is not TOKEN', () => { const recipientErrorState = { ...initialState, recipient: { warning: KNOWN_RECIPIENT_ADDRESS_WARNING, }, asset: { type: ASSET_TYPES.TOKEN, }, }; const action = { type: 'send/updateAsset', payload: { type: 'New Type', }, }; const result = sendReducer(recipientErrorState, action); expect(result.recipient.warning).not.toStrictEqual( recipientErrorState.recipient.warning, ); expect(result.recipient.warning).toBeNull(); }); it('should update asset type and details to TOKEN payload', () => { const action = { type: 'send/updateAsset', payload: { type: ASSET_TYPES.TOKEN, details: { address: '0xTokenAddress', decimals: 0, symbol: 'TKN', }, }, }; const result = sendReducer(initialState, action); expect(result.asset.type).toStrictEqual(action.payload.type); expect(result.asset.details).toStrictEqual(action.payload.details); }); }); describe('updateRecipient', () => { it('should', () => { const action = { type: 'send/updateRecipient', payload: { address: '0xNewAddress', }, }; const result = sendReducer(initialState, action); expect(result.stage).toStrictEqual(SEND_STAGES.DRAFT); expect(result.recipient.address).toStrictEqual(action.payload.address); }); }); describe('updateDraftTransaction', () => { describe('with LEGACY transactions', () => { it('should properly set fields', () => { const detailsForDraftTransactionState = { ...initialState, status: SEND_STATUSES.VALID, transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY, account: { address: '0xCurrentAddress', }, asset: { type: '', }, recipient: { address: '0xRecipientAddress', }, amount: { value: '0x1', }, gas: { gasPrice: '0x3b9aca00', // 1000000000 gasLimit: '0x5208', // 21000 }, }; const action = { type: 'send/updateDraftTransaction', }; const result = sendReducer(detailsForDraftTransactionState, action); expect(result.draftTransaction.txParams.to).toStrictEqual( detailsForDraftTransactionState.recipient.address, ); expect(result.draftTransaction.txParams.value).toStrictEqual( detailsForDraftTransactionState.amount.value, ); expect(result.draftTransaction.txParams.gas).toStrictEqual( detailsForDraftTransactionState.gas.gasLimit, ); expect(result.draftTransaction.txParams.gasPrice).toStrictEqual( detailsForDraftTransactionState.gas.gasPrice, ); }); it('should update the draftTransaction txParams recipient to token address when asset is type TOKEN', () => { const detailsForDraftTransactionState = { ...initialState, status: SEND_STATUSES.VALID, transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY, account: { address: '0xCurrentAddress', }, asset: { type: ASSET_TYPES.TOKEN, details: { address: '0xTokenAddress', }, }, amount: { value: '0x1', }, gas: { gasPrice: '0x3b9aca00', // 1000000000 gasLimit: '0x5208', // 21000 }, }; const action = { type: 'send/updateDraftTransaction', }; const result = sendReducer(detailsForDraftTransactionState, action); expect(result.draftTransaction.txParams.to).toStrictEqual( detailsForDraftTransactionState.asset.details.address, ); expect(result.draftTransaction.txParams.value).toStrictEqual('0x0'); expect(result.draftTransaction.txParams.gas).toStrictEqual( detailsForDraftTransactionState.gas.gasLimit, ); expect(result.draftTransaction.txParams.gasPrice).toStrictEqual( detailsForDraftTransactionState.gas.gasPrice, ); expect(result.draftTransaction.txParams.data).toStrictEqual( '0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', ); }); }); describe('with FEE_MARKET transactions', () => { it('should properly set fields', () => { const detailsForDraftTransactionState = { ...initialState, status: SEND_STATUSES.VALID, transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, account: { address: '0xCurrentAddress', }, asset: { type: '', }, recipient: { address: '0xRecipientAddress', }, amount: { value: '0x1', }, gas: { maxFeePerGas: '0x2540be400', // 10 GWEI maxPriorityFeePerGas: '0x3b9aca00', // 1 GWEI gasLimit: '0x5208', // 21000 }, eip1559support: true, }; const action = { type: 'send/updateDraftTransaction', }; const result = sendReducer(detailsForDraftTransactionState, action); expect(result.draftTransaction.txParams.to).toStrictEqual( detailsForDraftTransactionState.recipient.address, ); expect(result.draftTransaction.txParams.value).toStrictEqual( detailsForDraftTransactionState.amount.value, ); expect(result.draftTransaction.txParams.gas).toStrictEqual( detailsForDraftTransactionState.gas.gasLimit, ); expect(result.draftTransaction.txParams.gasPrice).toBeUndefined(); expect(result.draftTransaction.txParams.maxFeePerGas).toStrictEqual( detailsForDraftTransactionState.gas.maxFeePerGas, ); expect( result.draftTransaction.txParams.maxPriorityFeePerGas, ).toStrictEqual( detailsForDraftTransactionState.gas.maxPriorityFeePerGas, ); }); it('should update the draftTransaction txParams recipient to token address when asset is type TOKEN', () => { const detailsForDraftTransactionState = { ...initialState, status: SEND_STATUSES.VALID, transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, account: { address: '0xCurrentAddress', }, asset: { type: ASSET_TYPES.TOKEN, details: { address: '0xTokenAddress', }, }, amount: { value: '0x1', }, gas: { maxFeePerGas: '0x2540be400', // 10 GWEI maxPriorityFeePerGas: '0x3b9aca00', // 1 GWEI gasLimit: '0x5208', // 21000 }, eip1559support: true, }; const action = { type: 'send/updateDraftTransaction', }; const result = sendReducer(detailsForDraftTransactionState, action); expect(result.draftTransaction.txParams.to).toStrictEqual( detailsForDraftTransactionState.asset.details.address, ); expect(result.draftTransaction.txParams.value).toStrictEqual('0x0'); expect(result.draftTransaction.txParams.gas).toStrictEqual( detailsForDraftTransactionState.gas.gasLimit, ); expect(result.draftTransaction.txParams.maxFeePerGas).toStrictEqual( detailsForDraftTransactionState.gas.maxFeePerGas, ); expect( result.draftTransaction.txParams.maxPriorityFeePerGas, ).toStrictEqual( detailsForDraftTransactionState.gas.maxPriorityFeePerGas, ); expect(result.draftTransaction.txParams.gasPrice).toBeUndefined(); expect(result.draftTransaction.txParams.data).toStrictEqual( '0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', ); }); }); }); describe('useDefaultGas', () => { it('should', () => { const action = { type: 'send/useDefaultGas', }; const result = sendReducer(initialState, action); expect(result.gas.isCustomGasSet).toStrictEqual(false); }); }); describe('useCustomGas', () => { it('should', () => { const action = { type: 'send/useCustomGas', }; const result = sendReducer(initialState, action); expect(result.gas.isCustomGasSet).toStrictEqual(true); }); }); describe('updateRecipientUserInput', () => { it('should update recipient user input with payload', () => { const action = { type: 'send/updateRecipientUserInput', payload: 'user input', }; const result = sendReducer(initialState, action); expect(result.recipient.userInput).toStrictEqual(action.payload); }); }); describe('validateRecipientUserInput', () => { it('should set recipient error and warning to null when user input is', () => { const noUserInputState = { recipient: { mode: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS, userInput: '', error: 'someError', warning: 'someWarning', }, }; const action = { type: 'send/validateRecipientUserInput', }; const result = sendReducer(noUserInputState, action); expect(result.recipient.error).toBeNull(); expect(result.recipient.warning).toBeNull(); }); it('should error with an invalid address error when user input is not a valid hex string', () => { const tokenAssetTypeState = { ...initialState, recipient: { userInput: '0xValidateError', }, }; const action = { type: 'send/validateRecipientUserInput', payload: { chainId: '', tokens: [], useTokenDetection: true, tokenAddressList: [], }, }; const result = sendReducer(tokenAssetTypeState, action); expect(result.recipient.error).toStrictEqual('invalidAddressRecipient'); }); // TODO: Expectation might change in the future it('should error with an invalid network error when user input is not a valid hex string on a non default network', () => { const tokenAssetTypeState = { ...initialState, recipient: { userInput: '0xValidateError', }, }; const action = { type: 'send/validateRecipientUserInput', payload: { chainId: '0x55', tokens: [], useTokenDetection: true, tokenAddressList: [], }, }; const result = sendReducer(tokenAssetTypeState, action); expect(result.recipient.error).toStrictEqual( 'invalidAddressRecipientNotEthNetwork', ); }); it('should error with invalid address recipient when the user inputs the burn address', () => { const tokenAssetTypeState = { ...initialState, recipient: { userInput: '0x0000000000000000000000000000000000000000', }, }; const action = { type: 'send/validateRecipientUserInput', payload: { chainId: '', tokens: [], useTokenDetection: true, tokenAddressList: [], }, }; const result = sendReducer(tokenAssetTypeState, action); expect(result.recipient.error).toStrictEqual('invalidAddressRecipient'); }); it('should error with same address recipient as a token', () => { const tokenAssetTypeState = { ...initialState, asset: { type: ASSET_TYPES.TOKEN, details: { address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', }, }, recipient: { userInput: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', }, }; const action = { type: 'send/validateRecipientUserInput', payload: { chainId: '0x4', tokens: [], useTokenDetection: true, tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'], }, }; const result = sendReducer(tokenAssetTypeState, action); expect(result.recipient.error).toStrictEqual('contractAddressError'); }); }); describe('updateRecipientSearchMode', () => { it('should', () => { const action = { type: 'send/updateRecipientSearchMode', payload: 'a-random-string', }; const result = sendReducer(initialState, action); expect(result.recipient.mode).toStrictEqual(action.payload); }); }); describe('resetSendState', () => { it('should', () => { const action = { type: 'send/resetSendState', }; const result = sendReducer({}, action); expect(result).toStrictEqual(initialState); }); }); describe('validateAmountField', () => { it('should error with insufficient funds when amount asset value plust gas is higher than asset balance', () => { const nativeAssetState = { ...initialState, amount: { value: '0x6fc23ac0', // 1875000000 }, asset: { type: ASSET_TYPES.NATIVE, balance: '0x77359400', // 2000000000 }, gas: { gasTotal: '0x8f0d180', // 150000000 }, }; const action = { type: 'send/validateAmountField', }; const result = sendReducer(nativeAssetState, action); expect(result.amount.error).toStrictEqual(INSUFFICIENT_FUNDS_ERROR); }); it('should error with insufficient tokens when amount value of tokens is higher than asset balance of token', () => { const tokenAssetState = { ...initialState, amount: { value: '0x77359400', // 2000000000 }, asset: { type: ASSET_TYPES.TOKEN, balance: '0x6fc23ac0', // 1875000000 details: { decimals: 0, }, }, }; const action = { type: 'send/validateAmountField', }; const result = sendReducer(tokenAssetState, action); expect(result.amount.error).toStrictEqual(INSUFFICIENT_TOKENS_ERROR); }); it('should error negative value amount', () => { const negativeAmountState = { ...initialState, amount: { value: '-1', }, }; const action = { type: 'send/validateAmountField', }; const result = sendReducer(negativeAmountState, action); expect(result.amount.error).toStrictEqual(NEGATIVE_ETH_ERROR); }); it('should not error for positive value amount', () => { const otherState = { ...initialState, amount: { error: 'someError', value: '1', }, asset: { type: '', }, }; const action = { type: 'send/validateAmountField', }; const result = sendReducer(otherState, action); expect(result.amount.error).toBeNull(); }); }); describe('validateGasField', () => { it('should error when total amount of gas is higher than account balance', () => { const gasFieldState = { ...initialState, account: { balance: '0x0', }, gas: { gasTotal: '0x1319718a5000', // 21000000000000 }, }; const action = { type: 'send/validateGasField', }; const result = sendReducer(gasFieldState, action); expect(result.gas.error).toStrictEqual(INSUFFICIENT_FUNDS_ERROR); }); }); describe('validateSendState', () => { it('should set `INVALID` send state status when amount error is present', () => { const amountErrorState = { ...initialState, amount: { error: 'Some Amount Error', }, }; const action = { type: 'send/validateSendState', }; const result = sendReducer(amountErrorState, action); expect(result.status).toStrictEqual(SEND_STATUSES.INVALID); }); it('should set `INVALID` send state status when gas error is present', () => { const gasErrorState = { ...initialState, gas: { error: 'Some Amount Error', }, }; const action = { type: 'send/validateSendState', }; const result = sendReducer(gasErrorState, action); expect(result.status).toStrictEqual(SEND_STATUSES.INVALID); }); it('should set `INVALID` send state status when asset type is `TOKEN` without token details present', () => { const assetErrorState = { ...initialState, asset: { type: ASSET_TYPES.TOKEN, }, }; const action = { type: 'send/validateSendState', }; const result = sendReducer(assetErrorState, action); expect(result.status).toStrictEqual(SEND_STATUSES.INVALID); }); it('should set `INVALID` send state status when gasLimit is under the minimumGasLimit', () => { const gasLimitErroState = { ...initialState, gas: { gasLimit: '0x5207', minimumGasLimit: '0x5208', }, }; const action = { type: 'send/validateSendState', }; const result = sendReducer(gasLimitErroState, action); expect(result.status).toStrictEqual(SEND_STATUSES.INVALID); }); it('should set `VALID` send state status when conditionals have not been met', () => { const validSendStatusState = { ...initialState, stage: SEND_STAGES.DRAFT, asset: { type: ASSET_TYPES.TOKEN, details: { address: '0x000', }, }, gas: { isGasEstimateLoading: false, gasLimit: '0x5208', minimumGasLimit: '0x5208', }, }; const action = { type: 'send/validateSendState', }; const result = sendReducer(validSendStatusState, action); expect(result.status).toStrictEqual(SEND_STATUSES.VALID); }); }); }); describe('extraReducers/externalReducers', () => { describe('QR Code Detected', () => { const qrCodestate = { ...initialState, recipient: { address: '0xAddress', }, }; it('should set the recipient address to the scanned address value if they are not equal', () => { const action = { type: 'UI_QR_CODE_DETECTED', value: { type: 'address', values: { address: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc', }, }, }; const result = sendReducer(qrCodestate, action); expect(result.recipient.address).toStrictEqual( action.value.values.address, ); }); it('should not set the recipient address to invalid scanned address and errors', () => { const badQRAddressAction = { type: 'UI_QR_CODE_DETECTED', value: { type: 'address', values: { address: '0xBadAddress', }, }, }; const result = sendReducer(qrCodestate, badQRAddressAction); expect(result.recipient.address).toStrictEqual( qrCodestate.recipient.address, ); expect(result.recipient.error).toStrictEqual( INVALID_RECIPIENT_ADDRESS_ERROR, ); }); }); describe('Selected Address Changed', () => { it('should update selected account address and balance on non-edit stages', () => { const olderState = { ...initialState, account: { balance: '0x0', address: '0xAddress', }, }; const action = { type: 'SELECTED_ACCOUNT_CHANGED', payload: { account: { address: '0xDifferentAddress', balance: '0x1', }, }, }; const result = sendReducer(olderState, action); expect(result.account.balance).toStrictEqual( action.payload.account.balance, ); expect(result.account.address).toStrictEqual( action.payload.account.address, ); }); }); describe('Account Changed', () => { it('should', () => { const accountsChangedState = { ...initialState, stage: SEND_STAGES.EDIT, account: { address: '0xAddress', balance: '0x0', }, }; const action = { type: 'ACCOUNT_CHANGED', payload: { account: { address: '0xAddress', balance: '0x1', }, }, }; const result = sendReducer(accountsChangedState, action); expect(result.account.balance).toStrictEqual( action.payload.account.balance, ); }); it(`should not edit account balance if action payload address is not the same as state's address`, () => { const accountsChangedState = { ...initialState, stage: SEND_STAGES.EDIT, account: { address: '0xAddress', balance: '0x0', }, }; const action = { type: 'ACCOUNT_CHANGED', payload: { account: { address: '0xDifferentAddress', balance: '0x1', }, }, }; const result = sendReducer(accountsChangedState, action); expect(result.account.address).not.toStrictEqual( action.payload.account.address, ); expect(result.account.balance).not.toStrictEqual( action.payload.account.balance, ); }); }); describe('Initialize Pending Send State', () => { let dispatchSpy; let getState; beforeEach(() => { dispatchSpy = jest.fn(); }); it('should dispatch async action thunk first with pending, then finally fulfilling from minimal state', async () => { getState = jest.fn().mockReturnValue({ metamask: { gasEstimateType: GAS_ESTIMATE_TYPES.NONE, gasFeeEstimates: {}, networkDetails: { EIPS: { 1559: true, }, }, selectedAddress: '0xAddress', identities: { '0xAddress': { address: '0xAddress' } }, keyrings: [ { type: 'HD Key Tree', accounts: ['0xAddress'], }, ], accounts: { '0xAddress': { address: '0xAddress', balance: '0x0', }, }, cachedBalances: { 0x4: { '0xAddress': '0x0', }, }, provider: { chainId: '0x4', }, useTokenDetection: true, tokenList: { 0x514910771af9ca656af840dff83e8264ecf986ca: { address: '0x514910771af9ca656af840dff83e8264ecf986ca', symbol: 'LINK', decimals: 18, name: 'Chainlink', iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png', aggregators: [ 'airswapLight', 'bancor', 'cmc', 'coinGecko', 'kleros', 'oneInch', 'paraswap', 'pmm', 'totle', 'zapper', 'zerion', 'zeroEx', ], occurrences: 12, }, }, }, send: initialState, gas: { basicEstimateStatus: 'LOADING', basicEstimatesStatus: { safeLow: null, average: null, fast: null, }, }, }); const action = initializeSendState(); await action(dispatchSpy, getState, undefined); expect(dispatchSpy).toHaveBeenCalledTimes(3); expect(dispatchSpy.mock.calls[0][0].type).toStrictEqual( 'send/initializeSendState/pending', ); expect(dispatchSpy.mock.calls[2][0].type).toStrictEqual( 'send/initializeSendState/fulfilled', ); }); }); describe('Set Basic Gas Estimate Data', () => { it('should recalculate gas based off of average basic estimate data', () => { const gasState = { ...initialState, gas: { gasPrice: '0x0', gasPriceEstimate: '0x0', gasLimit: '0x5208', gasTotal: '0x0', minimumGasLimit: '0x5208', }, }; const action = { type: 'GAS_FEE_ESTIMATES_UPDATED', payload: { gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, gasFeeEstimates: { medium: '1', }, }, }; const result = sendReducer(gasState, action); expect(result.gas.gasPrice).toStrictEqual('0x3b9aca00'); // 1000000000 expect(result.gas.gasLimit).toStrictEqual(gasState.gas.gasLimit); expect(result.gas.gasTotal).toStrictEqual('0x1319718a5000'); }); }); }); describe('Action Creators', () => { describe('updateGasPrice', () => { it('should update gas price and update draft transaction with validated state', async () => { const store = mockStore({ send: { gas: { gasPrice: undefined, }, }, }); const newGasPrice = '0x0'; await store.dispatch(updateGasPrice(newGasPrice)); const actionResult = store.getActions(); const expectedActionResult = [ { type: 'send/updateGasFees', payload: { gasPrice: '0x0', transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY, }, }, ]; expect(actionResult).toStrictEqual(expectedActionResult); }); }); describe('UpdateSendAmount', () => { const defaultSendAmountState = { send: { amount: { mode: undefined, }, asset: { type: '', }, }, }; it('should create an action to update send amount', async () => { const sendState = { metamask: { blockGasLimit: '', selectedAddress: '', provider: { chainId: '0x1', }, }, ...defaultSendAmountState.send, send: { asset: { details: {}, }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, }, }; const store = mockStore(sendState); const newSendAmount = 'aNewSendAmount'; await store.dispatch(updateSendAmount(newSendAmount)); const actionResult = store.getActions(); const expectedFirstActionResult = { type: 'send/updateSendAmount', payload: 'aNewSendAmount', }; expect(actionResult[0]).toStrictEqual(expectedFirstActionResult); expect(actionResult[1].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[2].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); it('should create an action to update send amount mode to `INPUT` when mode is `MAX`', async () => { const sendState = { metamask: { blockGasLimit: '', selectedAddress: '', provider: { chainId: '0x1', }, }, ...defaultSendAmountState.send, send: { asset: { details: {}, }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, }, }; const store = mockStore(sendState); await store.dispatch(updateSendAmount()); const actionResult = store.getActions(); const expectedFirstActionResult = { type: 'send/updateSendAmount', payload: undefined, }; expect(actionResult[0]).toStrictEqual(expectedFirstActionResult); expect(actionResult[1].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[2].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); it('should create an action computeEstimateGasLimit and change states from pending to fulfilled with token asset types', async () => { const tokenAssetTypeSendState = { metamask: { blockGasLimit: '', selectedAddress: '', provider: { chainId: '0x1', }, }, ...defaultSendAmountState.send, send: { asset: { type: ASSET_TYPES.TOKEN, details: {}, }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, }, }; const store = mockStore(tokenAssetTypeSendState); await store.dispatch(updateSendAmount()); const actionResult = store.getActions(); expect(actionResult).toHaveLength(4); expect(actionResult[0].type).toStrictEqual('send/updateSendAmount'); expect(actionResult[1].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[2].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); }); describe('UpdateSendAsset', () => { const defaultSendAssetState = { metamask: { blockGasLimit: '', selectedAddress: '', provider: { chainId: '0x1', }, }, send: { account: { balance: '', }, asset: { type: '', details: {}, }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, }, }; it('should create actions for updateSendAsset', async () => { const store = mockStore(defaultSendAssetState); const newSendAsset = { type: '', details: { address: '', symbol: '', decimals: '', }, }; await store.dispatch(updateSendAsset(newSendAsset)); const actionResult = store.getActions(); expect(actionResult).toHaveLength(4); expect(actionResult[0].type).toStrictEqual('send/updateAsset'); expect(actionResult[0].payload).toStrictEqual({ ...newSendAsset, balance: '', error: null, }); expect(actionResult[1].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[2].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); it('should create actions for updateSendAsset with tokens', async () => { global.eth = { contract: sinon.stub().returns({ at: sinon.stub().returns({ balanceOf: sinon.stub().returns(undefined), }), }), }; const store = mockStore(defaultSendAssetState); const newSendAsset = { type: ASSET_TYPES.TOKEN, details: { address: 'tokenAddress', symbol: 'tokenSymbol', decimals: 'tokenDecimals', }, }; await store.dispatch(updateSendAsset(newSendAsset)); const actionResult = store.getActions(); expect(actionResult).toHaveLength(6); expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION'); expect(actionResult[2].payload).toStrictEqual({ ...newSendAsset, balance: '0x0', error: null, }); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[4].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[5].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); it('should show ConvertTokenToNFT modal and throw "invalidAssetType" error when token passed in props is an ERC721 or ERC1155', async () => { process.env.COLLECTIBLES_V1 = true; getTokenStandardAndDetailsStub.mockImplementation(() => Promise.resolve({ standard: 'ERC1155' }), ); const store = mockStore(defaultSendAssetState); const newSendAsset = { type: ASSET_TYPES.TOKEN, details: { address: 'tokenAddress', symbol: 'tokenSymbol', decimals: 'tokenDecimals', }, }; await expect(() => store.dispatch(updateSendAsset(newSendAsset)), ).rejects.toThrow('invalidAssetType'); const actionResult = store.getActions(); expect(actionResult).toHaveLength(3); expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION'); expect(actionResult[2]).toStrictEqual({ payload: { name: 'CONVERT_TOKEN_TO_NFT', tokenAddress: 'tokenAddress', }, type: 'UI_MODAL_OPEN', }); process.env.COLLECTIBLES_V1 = false; }); }); describe('updateRecipientUserInput', () => { const updateRecipientUserInputState = { metamask: { provider: { chainId: '', }, tokens: [], useTokenDetection: true, tokenList: { '0x514910771af9ca656af840dff83e8264ecf986ca': { address: '0x514910771af9ca656af840dff83e8264ecf986ca', symbol: 'LINK', decimals: 18, name: 'Chainlink', iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png', aggregators: [ 'airswapLight', 'bancor', 'cmc', 'coinGecko', 'kleros', 'oneInch', 'paraswap', 'pmm', 'totle', 'zapper', 'zerion', 'zeroEx', ], occurrences: 12, }, }, }, }; it('should create actions for updateRecipientUserInput and checks debounce for validation', async () => { const clock = sinon.useFakeTimers(); const store = mockStore(updateRecipientUserInputState); const newUserRecipientInput = 'newUserRecipientInput'; await store.dispatch(updateRecipientUserInput(newUserRecipientInput)); expect(store.getActions()).toHaveLength(1); expect(store.getActions()[0].type).toStrictEqual( 'send/updateRecipientUserInput', ); expect(store.getActions()[0].payload).toStrictEqual( newUserRecipientInput, ); clock.tick(300); // debounce expect(store.getActions()).toHaveLength(2); expect(store.getActions()[1].type).toStrictEqual( 'send/validateRecipientUserInput', ); expect(store.getActions()[1].payload).toStrictEqual({ chainId: '', tokens: [], useTokenDetection: true, tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'], }); }); }); describe('useContactListForRecipientSearch', () => { it('should create action to change send recipient search to contact list', async () => { const store = mockStore(); await store.dispatch(useContactListForRecipientSearch()); const actionResult = store.getActions(); expect(actionResult).toStrictEqual([ { type: 'send/updateRecipientSearchMode', payload: RECIPIENT_SEARCH_MODES.CONTACT_LIST, }, ]); }); }); describe('UseMyAccountsForRecipientSearch', () => { it('should create action to change send recipient search to derived accounts', async () => { const store = mockStore(); await store.dispatch(useMyAccountsForRecipientSearch()); const actionResult = store.getActions(); expect(actionResult).toStrictEqual([ { type: 'send/updateRecipientSearchMode', payload: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS, }, ]); }); }); describe('UpdateRecipient', () => { const recipient = { address: '', nickname: '', }; it('should create actions to update recipient and recalculate gas limit if the asset type is not set', async () => { global.eth = { getCode: sinon.stub(), }; const updateRecipientState = { metamask: { addressBook: {}, identities: {}, provider: { chainId: '0x1', }, }, send: { account: { balance: '', }, asset: { type: '', }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, }, }; const store = mockStore(updateRecipientState); await store.dispatch(updateRecipient(recipient)); const actionResult = store.getActions(); expect(actionResult).toHaveLength(4); expect(actionResult[0].type).toStrictEqual('send/updateRecipient'); expect(actionResult[1].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[2].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); it('should update recipient nickname if the passed address exists in the addressBook state but no nickname param is provided', async () => { global.eth = { getCode: sinon.stub(), }; const TEST_RECIPIENT_ADDRESS = '0x0000000000000000000000000000000000000001'; const TEST_RECIPIENT_NAME = 'The 1 address'; const updateRecipientState = { metamask: { addressBook: { '0x1': [ { address: TEST_RECIPIENT_ADDRESS, name: TEST_RECIPIENT_NAME, }, ], }, provider: { chainId: '0x1', }, }, send: { account: { balance: '', }, asset: { type: '', }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, }, }; const store = mockStore(updateRecipientState); await store.dispatch( updateRecipient({ address: '0x0000000000000000000000000000000000000001', nickname: '', }), ); const actionResult = store.getActions(); expect(actionResult).toHaveLength(4); expect(actionResult[0].type).toStrictEqual('send/updateRecipient'); expect(actionResult[0].payload.address).toStrictEqual( TEST_RECIPIENT_ADDRESS, ); expect(actionResult[0].payload.nickname).toStrictEqual( TEST_RECIPIENT_NAME, ); }); it('should create actions to reset recipient input and ens, calculate gas and then validate input', async () => { const tokenState = { metamask: { addressBook: {}, identities: {}, blockGasLimit: '', selectedAddress: '', provider: { chainId: '0x1', }, }, send: { account: { balance: '', }, asset: { type: ASSET_TYPES.TOKEN, details: {}, }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, }, }; const store = mockStore(tokenState); await store.dispatch(updateRecipient(recipient)); const actionResult = store.getActions(); expect(actionResult).toHaveLength(4); expect(actionResult[0].type).toStrictEqual('send/updateRecipient'); expect(actionResult[1].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[2].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); }); describe('ResetRecipientInput', () => { it('should create actions to reset recipient input and ens then validates input', async () => { const updateRecipientState = { metamask: { addressBook: {}, identities: {}, provider: { chainId: '', }, tokens: [], useTokenDetection: true, tokenList: { 0x514910771af9ca656af840dff83e8264ecf986ca: { address: '0x514910771af9ca656af840dff83e8264ecf986ca', symbol: 'LINK', decimals: 18, name: 'Chainlink', iconUrl: 'https://s3.amazonaws.com/airswap-token-images/LINK.png', aggregators: [ 'airswapLight', 'bancor', 'cmc', 'coinGecko', 'kleros', 'oneInch', 'paraswap', 'pmm', 'totle', 'zapper', 'zerion', 'zeroEx', ], occurrences: 12, }, }, }, send: { asset: { type: '', }, recipient: { address: 'Address', nickname: 'NickName', }, gas: { gasPrice: '0x1', }, amount: { value: '0x1', }, draftTransaction: {}, }, }; const store = mockStore(updateRecipientState); await store.dispatch(resetRecipientInput()); const actionResult = store.getActions(); expect(actionResult).toHaveLength(7); expect(actionResult[0].type).toStrictEqual( 'send/updateRecipientUserInput', ); expect(actionResult[0].payload).toStrictEqual(''); expect(actionResult[1].type).toStrictEqual('send/updateRecipient'); expect(actionResult[2].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[3].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[4].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); expect(actionResult[5].type).toStrictEqual('ENS/resetEnsResolution'); expect(actionResult[6].type).toStrictEqual( 'send/validateRecipientUserInput', ); }); }); describe('UpdateSendHexData', () => { const sendHexDataState = { send: { asset: { type: '', }, }, }; it('should create action to update hexData', async () => { const hexData = '0x1'; const store = mockStore(sendHexDataState); await store.dispatch(updateSendHexData(hexData)); const actionResult = store.getActions(); const expectActionResult = [ { type: 'send/updateUserInputHexData', payload: hexData }, ]; expect(actionResult).toHaveLength(1); expect(actionResult).toStrictEqual(expectActionResult); }); }); describe('ToggleSendMaxMode', () => { it('should create actions to toggle update max mode when send amount mode is not max', async () => { const sendMaxModeState = { send: { asset: { type: ASSET_TYPES.TOKEN, details: {}, }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { mode: '', value: '', }, draftTransaction: { userInputHexData: '', }, }, metamask: { provider: { chainId: RINKEBY_CHAIN_ID, }, }, }; const store = mockStore(sendMaxModeState); await store.dispatch(toggleSendMaxMode()); const actionResult = store.getActions(); expect(actionResult).toHaveLength(5); expect(actionResult[0].type).toStrictEqual('send/updateAmountMode'); expect(actionResult[1].type).toStrictEqual('send/updateAmountToMax'); expect(actionResult[2].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[3].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[4].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); it('should create actions to toggle off max mode when send amount mode is max', async () => { const sendMaxModeState = { send: { asset: { type: ASSET_TYPES.TOKEN, details: {}, }, gas: { gasPrice: '', }, recipient: { address: '', }, amount: { mode: AMOUNT_MODES.MAX, value: '', }, draftTransaction: { userInputHexData: '', }, }, metamask: { provider: { chainId: RINKEBY_CHAIN_ID, }, }, }; const store = mockStore(sendMaxModeState); await store.dispatch(toggleSendMaxMode()); const actionResult = store.getActions(); expect(actionResult).toHaveLength(5); expect(actionResult[0].type).toStrictEqual('send/updateAmountMode'); expect(actionResult[1].type).toStrictEqual('send/updateSendAmount'); expect(actionResult[2].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[3].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[4].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); }); }); describe('SignTransaction', () => { const signTransactionState = { send: { asset: {}, stage: '', draftTransaction: {}, recipient: {}, amount: {}, account: {}, }, }; it('should show confirm tx page when no other conditions for signing have been met', async () => { global.ethQuery = { sendTransaction: sinon.stub(), }; const store = mockStore(signTransactionState); await store.dispatch(signTransaction()); const actionResult = store.getActions(); expect(actionResult).toHaveLength(1); expect(actionResult[0].type).toStrictEqual('SHOW_CONF_TX_PAGE'); }); it('should create actions for updateTransaction rejecting', async () => { const editStageSignTxState = { metamask: { unapprovedTxs: { 1: { id: 1, txParams: { value: 'oldTxValue', }, }, }, }, send: { ...signTransactionState.send, stage: SEND_STAGES.EDIT, draftTransaction: { id: 1, txParams: { value: 'newTxValue', }, }, }, }; jest.mock('../../store/actions.js'); const store = mockStore(editStageSignTxState); await store.dispatch(signTransaction()); const actionResult = store.getActions(); expect(actionResult).toHaveLength(5); expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); expect(actionResult[1].type).toStrictEqual('UPDATE_TRANSACTION_PARAMS'); expect(actionResult[2].type).toStrictEqual('HIDE_LOADING_INDICATION'); }); }); describe('editTransaction', () => { it('should set up the appropriate state for editing a native asset transaction', async () => { const editTransactionState = { metamask: { provider: { chainId: RINKEBY_CHAIN_ID, }, tokens: [], addressBook: { [RINKEBY_CHAIN_ID]: {}, }, identities: {}, unapprovedTxs: { 1: { id: 1, txParams: { data: '', from: '0xAddress', to: '0xRecipientAddress', gas: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', // 1000000000 value: '0xde0b6b3a7640000', // 1000000000000000000 }, }, }, }, send: { asset: { type: '', }, recipient: { address: 'Address', nickname: 'NickName', }, }, }; const store = mockStore(editTransactionState); await store.dispatch(editTransaction(ASSET_TYPES.NATIVE, 1)); const actionResult = store.getActions(); expect(actionResult).toHaveLength(1); expect(actionResult[0].type).toStrictEqual('send/editTransaction'); expect(actionResult[0].payload).toStrictEqual({ address: '0xRecipientAddress', amount: '0xde0b6b3a7640000', data: '', from: '0xAddress', gasLimit: GAS_LIMITS.SIMPLE, gasPrice: '0x3b9aca00', id: 1, nickname: '', }); const action = actionResult[0]; const result = sendReducer(initialState, action); expect(result.gas.gasLimit).toStrictEqual(action.payload.gasLimit); expect(result.gas.gasPrice).toStrictEqual(action.payload.gasPrice); expect(result.amount.value).toStrictEqual(action.payload.amount); expect(result.draftTransaction.txParams.to).toStrictEqual( action.payload.address, ); expect(result.draftTransaction.txParams.value).toStrictEqual( action.payload.amount, ); expect(result.draftTransaction.txParams.gasPrice).toStrictEqual( action.payload.gasPrice, ); expect(result.draftTransaction.txParams.gas).toStrictEqual( action.payload.gasLimit, ); }); it('should set up the appropriate state for editing a collectible asset transaction', async () => { const editTransactionState = { metamask: { blockGasLimit: '0x3a98', selectedAddress: '', provider: { chainId: RINKEBY_CHAIN_ID, }, tokens: [], addressBook: { [RINKEBY_CHAIN_ID]: {}, }, identities: {}, unapprovedTxs: { 1: { id: 1, txParams: { data: '', from: '0xAddress', to: '0xTokenAddress', gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE, gasPrice: '0x3b9aca00', // 1000000000 value: '0x0', }, }, }, }, send: { account: { address: '0xAddress', balance: '0x0', }, asset: { type: '', }, gas: { gasPrice: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, recipient: { address: 'Address', nickname: 'NickName', }, }, }; global.eth = { contract: sinon.stub().returns({ at: sinon.stub().returns({ balanceOf: sinon.stub().returns(undefined), }), }), getCode: jest.fn(() => '0xa'), }; const store = mockStore(editTransactionState); await store.dispatch( editTransaction( ASSET_TYPES.COLLECTIBLE, 1, { name: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM, args: { _to: '0xRecipientAddress', _value: ethers.BigNumber.from(15000), }, }, { address: '0xf5de760f2e916647fd766B4AD9E85ff943cE3A2b', description: 'A test NFT dispensed from faucet.paradigm.xyz.', image: 'https://ipfs.io/ipfs/bafybeifvwitulq6elvka2hoqhwixfhgb42l4aiukmtrw335osetikviuuu', name: 'MultiFaucet Test NFT', standard: 'ERC721', tokenId: '26847', }, ), ); const actionResult = store.getActions(); expect(actionResult).toHaveLength(5); expect(actionResult[0].type).toStrictEqual('send/updateAsset'); expect(actionResult[0].payload).toStrictEqual({ balance: '0x1', type: ASSET_TYPES.COLLECTIBLE, error: null, details: { address: '0xTokenAddress', description: 'A test NFT dispensed from faucet.paradigm.xyz.', image: 'https://ipfs.io/ipfs/bafybeifvwitulq6elvka2hoqhwixfhgb42l4aiukmtrw335osetikviuuu', name: 'MultiFaucet Test NFT', standard: 'ERC721', tokenId: '26847', }, }); expect(actionResult[1].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[2].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); expect(actionResult[4].type).toStrictEqual('send/editTransaction'); const action = actionResult[4]; const result = sendReducer(initialState, action); expect(result.gas.gasLimit).toStrictEqual(action.payload.gasLimit); expect(result.gas.gasPrice).toStrictEqual(action.payload.gasPrice); expect(result.amount.value).toStrictEqual(action.payload.amount); expect(result.draftTransaction.txParams.to).toStrictEqual( action.payload.address, ); expect(result.draftTransaction.txParams.value).toStrictEqual( action.payload.amount, ); expect(result.draftTransaction.txParams.gasPrice).toStrictEqual( action.payload.gasPrice, ); expect(result.draftTransaction.txParams.gas).toStrictEqual( action.payload.gasLimit, ); }); }); it('should set up the appropriate state for editing a token asset transaction', async () => { const editTransactionState = { metamask: { blockGasLimit: '0x3a98', selectedAddress: '', provider: { chainId: RINKEBY_CHAIN_ID, }, tokens: [], addressBook: { [RINKEBY_CHAIN_ID]: {}, }, identities: {}, unapprovedTxs: { 1: { id: 1, txParams: { data: '', from: '0xAddress', to: '0xTokenAddress', gas: GAS_LIMITS.BASE_TOKEN_ESTIMATE, gasPrice: '0x3b9aca00', // 1000000000 value: '0x0', }, }, }, }, send: { account: { address: '0xAddress', balance: '0x0', }, asset: { type: '', }, gas: { gasPrice: '', }, amount: { value: '', }, draftTransaction: { userInputHexData: '', }, recipient: { address: 'Address', nickname: 'NickName', }, }, }; global.eth = { contract: sinon.stub().returns({ at: sinon.stub().returns({ balanceOf: sinon.stub().returns(undefined), }), }), getCode: jest.fn(() => '0xa'), }; const store = mockStore(editTransactionState); await store.dispatch( editTransaction( ASSET_TYPES.TOKEN, 1, { name: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER, args: { _to: '0xRecipientAddress', _value: ethers.BigNumber.from(15000), }, }, { address: '0xAddress', symbol: 'SYMB', decimals: 18 }, ), ); const actionResult = store.getActions(); expect(actionResult).toHaveLength(7); expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION'); expect(actionResult[2].type).toStrictEqual('send/updateAsset'); expect(actionResult[2].payload).toStrictEqual({ balance: '0x0', type: ASSET_TYPES.TOKEN, error: null, details: { address: '0xTokenAddress', decimals: 18, symbol: 'SYMB', standard: 'ERC20', }, }); expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); expect(actionResult[4].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); expect(actionResult[5].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); expect(actionResult[6].type).toStrictEqual('send/editTransaction'); expect(actionResult[6].payload).toStrictEqual({ address: '0xrecipientaddress', // getting address from tokenData does .toLowerCase amount: '0x3a98', data: '', from: '0xAddress', gasLimit: GAS_LIMITS.BASE_TOKEN_ESTIMATE, gasPrice: '0x3b9aca00', id: 1, nickname: '', }); const action = actionResult[6]; const result = sendReducer(initialState, action); expect(result.gas.gasLimit).toStrictEqual(action.payload.gasLimit); expect(result.gas.gasPrice).toStrictEqual(action.payload.gasPrice); expect(result.amount.value).toStrictEqual(action.payload.amount); expect(result.draftTransaction.txParams.to).toStrictEqual( action.payload.address, ); expect(result.draftTransaction.txParams.value).toStrictEqual( action.payload.amount, ); expect(result.draftTransaction.txParams.gasPrice).toStrictEqual( action.payload.gasPrice, ); expect(result.draftTransaction.txParams.gas).toStrictEqual( action.payload.gasLimit, ); }); }); describe('selectors', () => { describe('gas selectors', () => { it('has a selector that gets gasLimit', () => { expect(getGasLimit({ send: initialState })).toBe('0x0'); }); it('has a selector that gets gasPrice', () => { expect(getGasPrice({ send: initialState })).toBe('0x0'); }); it('has a selector that gets gasTotal', () => { expect(getGasTotal({ send: initialState })).toBe('0x0'); }); it('has a selector to determine if gas fee is in error', () => { expect(gasFeeIsInError({ send: initialState })).toBe(false); expect( gasFeeIsInError({ send: { ...initialState, gas: { ...initialState.gas, error: 'yes', }, }, }), ).toBe(true); }); it('has a selector that gets minimumGasLimit', () => { expect(getMinimumGasLimitForSend({ send: initialState })).toBe( GAS_LIMITS.SIMPLE, ); }); describe('getGasInputMode selector', () => { it('returns BASIC when on mainnet and advanced inline gas is false', () => { expect( getGasInputMode({ metamask: { provider: { chainId: MAINNET_CHAIN_ID }, featureFlags: { advancedInlineGas: false }, }, send: initialState, }), ).toBe(GAS_INPUT_MODES.BASIC); }); it('returns BASIC when on localhost and advanced inline gas is false and IN_TEST is set', () => { process.env.IN_TEST = true; expect( getGasInputMode({ metamask: { provider: { chainId: '0x539' }, featureFlags: { advancedInlineGas: false }, }, send: initialState, }), ).toBe(GAS_INPUT_MODES.BASIC); process.env.IN_TEST = false; }); it('returns INLINE when on mainnet and advanced inline gas is true', () => { expect( getGasInputMode({ metamask: { provider: { chainId: MAINNET_CHAIN_ID }, featureFlags: { advancedInlineGas: true }, }, send: initialState, }), ).toBe(GAS_INPUT_MODES.INLINE); }); it('returns INLINE when on mainnet and advanced inline gas is false but eth_gasPrice estimate is used', () => { expect( getGasInputMode({ metamask: { provider: { chainId: MAINNET_CHAIN_ID }, featureFlags: { advancedInlineGas: false }, gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, }, send: initialState, }), ).toBe(GAS_INPUT_MODES.INLINE); }); it('returns INLINE when on mainnet and advanced inline gas is false but eth_gasPrice estimate is used even IN_TEST', () => { process.env.IN_TEST = true; expect( getGasInputMode({ metamask: { provider: { chainId: MAINNET_CHAIN_ID }, featureFlags: { advancedInlineGas: false }, gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, }, send: initialState, }), ).toBe(GAS_INPUT_MODES.INLINE); process.env.IN_TEST = false; }); it('returns CUSTOM if isCustomGasSet is true', () => { expect( getGasInputMode({ metamask: { provider: { chainId: MAINNET_CHAIN_ID }, featureFlags: { advancedInlineGas: true }, }, send: { ...initialState, gas: { ...initialState.send, isCustomGasSet: true, }, }, }), ).toBe(GAS_INPUT_MODES.CUSTOM); }); }); }); describe('asset selectors', () => { it('has a selector to get the asset', () => { expect(getSendAsset({ send: initialState })).toMatchObject( initialState.asset, ); }); it('has a selector to get the asset address', () => { expect( getSendAssetAddress({ send: { ...initialState, asset: { balance: '0x0', details: { address: '0x0' }, type: ASSET_TYPES.TOKEN, }, }, }), ).toBe('0x0'); }); it('has a selector that determines if asset is sendable based on ERC721 status', () => { expect(getIsAssetSendable({ send: initialState })).toBe(true); expect( getIsAssetSendable({ send: { ...initialState, asset: { ...initialState, type: ASSET_TYPES.TOKEN, details: { isERC721: true }, }, }, }), ).toBe(false); }); }); describe('amount selectors', () => { it('has a selector to get send amount', () => { expect(getSendAmount({ send: initialState })).toBe('0x0'); }); it('has a selector to get if there is an insufficient funds error', () => { expect(getIsBalanceInsufficient({ send: initialState })).toBe(false); expect( getIsBalanceInsufficient({ send: { ...initialState, gas: { ...initialState.gas, error: INSUFFICIENT_FUNDS_ERROR }, }, }), ).toBe(true); }); it('has a selector to get max mode state', () => { expect(getSendMaxModeState({ send: initialState })).toBe(false); expect( getSendMaxModeState({ send: { ...initialState, amount: { ...initialState.amount, mode: AMOUNT_MODES.MAX }, }, }), ).toBe(true); }); it('has a selector to get the draft transaction ID', () => { expect(getDraftTransactionID({ send: initialState })).toBeNull(); expect( getDraftTransactionID({ send: { ...initialState, draftTransaction: { ...initialState.draftTransaction, id: 'ID', }, }, }), ).toBe('ID'); }); it('has a selector to get the user entered hex data', () => { expect(getSendHexData({ send: initialState })).toBeNull(); expect( getSendHexData({ send: { ...initialState, draftTransaction: { ...initialState.draftTransaction, userInputHexData: '0x0', }, }, }), ).toBe('0x0'); }); it('has a selector to get if there is an amount error', () => { expect(sendAmountIsInError({ send: initialState })).toBe(false); expect( sendAmountIsInError({ send: { ...initialState, amount: { ...initialState.amount, error: 'any' }, }, }), ).toBe(true); }); }); describe('recipient selectors', () => { it('has a selector to get recipient address', () => { expect(getSendTo({ send: initialState })).toBe(''); expect( getSendTo({ send: { ...initialState, recipient: { ...initialState.recipient, address: '0xb' }, }, }), ).toBe('0xb'); }); it('has a selector to check if using the my accounts option for recipient selection', () => { expect( getIsUsingMyAccountForRecipientSearch({ send: initialState }), ).toBe(false); expect( getIsUsingMyAccountForRecipientSearch({ send: { ...initialState, recipient: { ...initialState.recipient, mode: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS, }, }, }), ).toBe(true); }); it('has a selector to get recipient user input in input field', () => { expect(getRecipientUserInput({ send: initialState })).toBe(''); expect( getRecipientUserInput({ send: { ...initialState, recipient: { ...initialState.recipient, userInput: 'domain.eth', }, }, }), ).toBe('domain.eth'); }); it('has a selector to get recipient state', () => { expect(getRecipient({ send: initialState })).toMatchObject( initialState.recipient, ); }); }); describe('send validity selectors', () => { it('has a selector to get send errors', () => { expect(getSendErrors({ send: initialState })).toMatchObject({ gasFee: null, amount: null, }); expect( getSendErrors({ send: { ...initialState, gas: { ...initialState.gas, error: 'gasFeeTest', }, amount: { ...initialState.amount, error: 'amountTest', }, }, }), ).toMatchObject({ gasFee: 'gasFeeTest', amount: 'amountTest' }); }); it('has a selector to get send state initialization status', () => { expect(isSendStateInitialized({ send: initialState })).toBe(false); expect( isSendStateInitialized({ send: { ...initialState, stage: SEND_STATUSES.ADD_RECIPIENT, }, }), ).toBe(true); }); it('has a selector to get send state validity', () => { expect(isSendFormInvalid({ send: initialState })).toBe(false); expect( isSendFormInvalid({ send: { ...initialState, status: SEND_STATUSES.INVALID }, }), ).toBe(true); }); it('has a selector to get send stage', () => { expect(getSendStage({ send: initialState })).toBe(SEND_STAGES.INACTIVE); expect( getSendStage({ send: { ...initialState, stage: SEND_STAGES.ADD_RECIPIENT }, }), ).toBe(SEND_STAGES.ADD_RECIPIENT); }); }); }); });