From 564ad2f81be31f98ac71c61a849904e71db8f1a4 Mon Sep 17 00:00:00 2001 From: Brad Decker Date: Wed, 4 May 2022 11:54:46 -0500 Subject: [PATCH] Track send flow history on txMeta (#14510) --- app/scripts/controllers/transactions/index.js | 38 ++- .../transactions/tx-state-manager.js | 1 + app/scripts/metamask-controller.js | 3 + ui/ducks/send/send.js | 100 +++++- ui/ducks/send/send.test.js | 293 +++++++++++++----- .../add-recipient/add-recipient.component.js | 20 +- .../add-recipient/add-recipient.container.js | 2 + ui/pages/send/send.js | 19 +- ui/store/actions.js | 30 ++ 9 files changed, 415 insertions(+), 91 deletions(-) diff --git a/app/scripts/controllers/transactions/index.js b/app/scripts/controllers/transactions/index.js index e982b962f..e5d50375f 100644 --- a/app/scripts/controllers/transactions/index.js +++ b/app/scripts/controllers/transactions/index.js @@ -647,6 +647,35 @@ export default class TransactionController extends EventEmitter { return this._getTransaction(txId); } + /** + * append new sendFlowHistory to the transaction with id if the transaction + * state is unapproved. Returns the updated transaction. + * + * @param {string} txId - transaction id + * @param {Array<{ entry: string, timestamp: number }>} sendFlowHistory - + * history to add to the sendFlowHistory property of txMeta. + * @returns {TransactionMeta} the txMeta of the updated transaction + */ + updateTransactionSendFlowHistory(txId, sendFlowHistory) { + this._throwErrorIfNotUnapprovedTx(txId, 'updateTransactionSendFlowHistory'); + const txMeta = this._getTransaction(txId); + + // only update what is defined + const note = `Update sendFlowHistory for ${txId}`; + + this.txStateManager.updateTransaction( + { + ...txMeta, + sendFlowHistory: [ + ...(txMeta?.sendFlowHistory ?? []), + ...sendFlowHistory, + ], + }, + note, + ); + return this._getTransaction(txId); + } + // ==================================================================================================================================================== /** @@ -656,9 +685,15 @@ export default class TransactionController extends EventEmitter { * @param txParams * @param origin * @param transactionType + * @param sendFlowHistory * @returns {txMeta} */ - async addUnapprovedTransaction(txParams, origin, transactionType) { + async addUnapprovedTransaction( + txParams, + origin, + transactionType, + sendFlowHistory = [], + ) { if ( transactionType !== undefined && !VALID_UNAPPROVED_TRANSACTION_TYPES.includes(transactionType) @@ -683,6 +718,7 @@ export default class TransactionController extends EventEmitter { let txMeta = this.txStateManager.generateTxMeta({ txParams: normalizedTxParams, origin, + sendFlowHistory, }); if (origin === ORIGIN_METAMASK) { diff --git a/app/scripts/controllers/transactions/tx-state-manager.js b/app/scripts/controllers/transactions/tx-state-manager.js index 111392fad..33e23bbf8 100644 --- a/app/scripts/controllers/transactions/tx-state-manager.js +++ b/app/scripts/controllers/transactions/tx-state-manager.js @@ -127,6 +127,7 @@ export default class TransactionStateManager extends EventEmitter { chainId, loadingDefaults: true, dappSuggestedGasFees, + sendFlowHistory: [], ...opts, }; } diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 8017579a6..d8a67d3b6 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1637,6 +1637,9 @@ export default class MetamaskController extends EventEmitter { updateTransactionGasFees: txController.updateTransactionGasFees.bind( txController, ), + updateTransactionSendFlowHistory: txController.updateTransactionSendFlowHistory.bind( + txController, + ), updateSwapApprovalTransaction: txController.updateSwapApprovalTransaction.bind( txController, diff --git a/ui/ducks/send/send.js b/ui/ducks/send/send.js index 76546ac04..a812e34a1 100644 --- a/ui/ducks/send/send.js +++ b/ui/ducks/send/send.js @@ -60,6 +60,7 @@ import { getTokenStandardAndDetails, showModal, addUnapprovedTransactionAndRouteToConfirmationPage, + updateTransactionSendFlowHistory, } from '../../store/actions'; import { setCustomGasLimit } from '../gas/gas.duck'; import { @@ -110,6 +111,7 @@ import { import { readAddressAsContract } from '../../../shared/modules/contract-utils'; import { INVALID_ASSET_TYPE } from '../../helpers/constants/error-keys'; import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; +import { getValueFromWeiHex } from '../../helpers/utils/confirm-tx.util'; // typedefs /** * @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction @@ -684,12 +686,19 @@ export const initialState = { // Layer 1 gas fee total on multi-layer fee networks layer1GasTotal: '0x0', }, + history: [], }; const slice = createSlice({ name, initialState, reducers: { + addHistoryEntry: (state, action) => { + state.history.push({ + entry: action.payload, + timestamp: Date.now(), + }); + }, /** * update current amount.value in state and run post update validation of * the amount field and the send state. Recomputes the draftTransaction @@ -1402,9 +1411,10 @@ const { updateGasLimit, validateRecipientUserInput, updateRecipientSearchMode, + addHistoryEntry, } = actions; -export { useDefaultGas, useCustomGas, updateGasLimit }; +export { useDefaultGas, useCustomGas, updateGasLimit, addHistoryEntry }; // Action Creators @@ -1421,6 +1431,9 @@ export { useDefaultGas, useCustomGas, updateGasLimit }; */ export function updateGasPrice(gasPrice) { return (dispatch) => { + dispatch( + addHistoryEntry(`sendFlow - user set legacy gasPrice to ${gasPrice}`), + ); dispatch( actions.updateGasFees({ gasPrice, @@ -1452,8 +1465,36 @@ export function resetSendState() { */ export function updateSendAmount(amount) { return async (dispatch, getState) => { - await dispatch(actions.updateSendAmount(amount)); const state = getState(); + let logAmount = amount; + if (state[name].asset.type === ASSET_TYPES.TOKEN) { + const multiplier = Math.pow( + 10, + Number(state[name].asset.details?.decimals || 0), + ); + const decimalValueString = conversionUtil(addHexPrefix(amount), { + fromNumericBase: 'hex', + toNumericBase: 'dec', + toCurrency: state[name].asset.details?.symbol, + conversionRate: multiplier, + invertConversionRate: true, + }); + + logAmount = `${Number(decimalValueString) ? decimalValueString : ''} ${ + state[name].asset.details?.symbol + }`; + } else { + const ethValue = getValueFromWeiHex({ + value: amount, + toCurrency: ETH, + numberOfDecimals: 8, + }); + logAmount = `${ethValue} ${ETH}`; + } + await dispatch( + addHistoryEntry(`sendFlow - user set amount to ${logAmount}`), + ); + await dispatch(actions.updateSendAmount(amount)); if (state.send.amount.mode === AMOUNT_MODES.MAX) { await dispatch(actions.updateAmountMode(AMOUNT_MODES.INPUT)); } @@ -1482,6 +1523,19 @@ export function updateSendAmount(amount) { */ export function updateSendAsset({ type, details }) { return async (dispatch, getState) => { + dispatch(addHistoryEntry(`sendFlow - user set asset type to ${type}`)); + dispatch( + addHistoryEntry( + `sendFlow - user set asset symbol to ${details?.symbol ?? 'undefined'}`, + ), + ); + dispatch( + addHistoryEntry( + `sendFlow - user set asset address to ${ + details?.address ?? 'undefined' + }`, + ), + ); const state = getState(); let { balance, error } = state.send.asset; const userAddress = state.send.account.address ?? getSelectedAddress(state); @@ -1580,6 +1634,11 @@ export function updateSendAsset({ type, details }) { * it only applicable for use within action creators. */ const debouncedValidateRecipientUserInput = debounce((dispatch, payload) => { + dispatch( + addHistoryEntry( + `sendFlow - user typed ${payload.userInput} into recipient input field`, + ), + ); dispatch(validateRecipientUserInput(payload)); }, 300); @@ -1600,6 +1659,7 @@ export function updateRecipientUserInput(userInput) { const useTokenDetection = getUseTokenDetection(state); const tokenAddressList = Object.keys(getTokenList(state)); debouncedValidateRecipientUserInput(dispatch, { + userInput, chainId, tokens, useTokenDetection, @@ -1610,12 +1670,22 @@ export function updateRecipientUserInput(userInput) { export function useContactListForRecipientSearch() { return (dispatch) => { + dispatch( + addHistoryEntry( + `sendFlow - user selected back to all on recipient screen`, + ), + ); dispatch(updateRecipientSearchMode(RECIPIENT_SEARCH_MODES.CONTACT_LIST)); }; } export function useMyAccountsForRecipientSearch() { return (dispatch) => { + dispatch( + addHistoryEntry( + `sendFlow - user selected transfer to my accounts on recipient screen`, + ), + ); dispatch(updateRecipientSearchMode(RECIPIENT_SEARCH_MODES.MY_ACCOUNTS)); }; } @@ -1638,6 +1708,8 @@ export function useMyAccountsForRecipientSearch() { */ export function updateRecipient({ address, nickname }) { return async (dispatch, getState) => { + // Do not addHistoryEntry here as this is called from a number of places + // each with significance to the user and transaction history. const state = getState(); const nicknameFromAddressBookEntryOrAccountName = getAddressBookEntryOrAccountName(state, address) ?? ''; @@ -1656,6 +1728,7 @@ export function updateRecipient({ address, nickname }) { */ export function resetRecipientInput() { return async (dispatch) => { + await dispatch(addHistoryEntry(`sendFlow - user cleared recipient input`)); await dispatch(updateRecipientUserInput('')); await dispatch(updateRecipient({ address: '', nickname: '' })); await dispatch(resetEnsResolution()); @@ -1675,6 +1748,9 @@ export function resetRecipientInput() { */ export function updateSendHexData(hexData) { return async (dispatch, getState) => { + await dispatch( + addHistoryEntry(`sendFlow - user added custom hexData ${hexData}`), + ); await dispatch(actions.updateUserInputHexData(hexData)); const state = getState(); if (state.send.asset.type === ASSET_TYPES.NATIVE) { @@ -1695,9 +1771,11 @@ export function toggleSendMaxMode() { if (state.send.amount.mode === AMOUNT_MODES.MAX) { await dispatch(actions.updateAmountMode(AMOUNT_MODES.INPUT)); await dispatch(actions.updateSendAmount('0x0')); + await dispatch(addHistoryEntry(`sendFlow - user toggled max mode off`)); } else { await dispatch(actions.updateAmountMode(AMOUNT_MODES.MAX)); await dispatch(actions.updateAmountToMax()); + await dispatch(addHistoryEntry(`sendFlow - user toggled max mode on`)); } await dispatch(computeEstimatedGasLimit()); }; @@ -1746,6 +1824,12 @@ export function signTransaction() { eip1559support ? eip1559OnlyTxParamsToUpdate : txParams, ), }; + await dispatch( + addHistoryEntry( + `sendFlow - user clicked next and transaction should be updated in controller`, + ), + ); + await dispatch(updateTransactionSendFlowHistory(id, state[name].history)); dispatch(updateEditableParams(id, editingTx.txParams)); dispatch(updateTransactionGasFees(id, editingTx.txParams)); } else { @@ -1757,10 +1841,17 @@ export function signTransaction() { ? TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM : TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER; } + await dispatch( + addHistoryEntry( + `sendFlow - user clicked next and transaction should be added to controller`, + ), + ); + dispatch( addUnapprovedTransactionAndRouteToConfirmationPage( txParams, transactionType, + state[name].history, ), ); } @@ -1775,6 +1866,11 @@ export function editTransaction( ) { return async (dispatch, getState) => { const state = getState(); + await dispatch( + addHistoryEntry( + `sendFlow - user clicked edit on transaction with id ${transactionId}`, + ), + ); const unapprovedTransactions = getUnapprovedTxs(state); const transaction = unapprovedTransactions[transactionId]; const { txParams } = transaction; diff --git a/ui/ducks/send/send.test.js b/ui/ducks/send/send.test.js index 8e16486db..a8466753b 100644 --- a/ui/ducks/send/send.test.js +++ b/ui/ducks/send/send.test.js @@ -80,9 +80,10 @@ jest.mock('./send', () => { setBackgroundConnection({ addPollingTokenToAppState: jest.fn(), - addUnapprovedTransaction: jest.fn((_x, _y, _z, cb) => { - return cb(null, {}); + addUnapprovedTransaction: jest.fn((_w, _x, _y, _z, cb) => { + cb(null); }), + updateTransactionSendFlowHistory: jest.fn((_x, _y, cb) => cb(null)), }); describe('Send Slice', () => { @@ -1247,6 +1248,10 @@ describe('Send Slice', () => { const actionResult = store.getActions(); const expectedActionResult = [ + { + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set legacy gasPrice to 0x0', + }, { type: 'send/updateGasFees', payload: { @@ -1302,22 +1307,28 @@ describe('Send Slice', () => { }; const store = mockStore(sendState); - const newSendAmount = 'aNewSendAmount'; + const newSendAmount = 'DE0B6B3A7640000'; await store.dispatch(updateSendAmount(newSendAmount)); const actionResult = store.getActions(); const expectedFirstActionResult = { + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set amount to 1 ETH', + }; + + const expectedSecondActionResult = { type: 'send/updateSendAmount', - payload: 'aNewSendAmount', + payload: 'DE0B6B3A7640000', }; expect(actionResult[0]).toStrictEqual(expectedFirstActionResult); - expect(actionResult[1].type).toStrictEqual( + expect(actionResult[1]).toStrictEqual(expectedSecondActionResult); + expect(actionResult[2].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[2].type).toStrictEqual( + expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); }); @@ -1358,15 +1369,21 @@ describe('Send Slice', () => { const actionResult = store.getActions(); const expectedFirstActionResult = { + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set amount to 0 ETH', + }; + + const expectedSecondActionResult = { type: 'send/updateSendAmount', payload: undefined, }; expect(actionResult[0]).toStrictEqual(expectedFirstActionResult); - expect(actionResult[1].type).toStrictEqual( + expect(actionResult[1]).toStrictEqual(expectedSecondActionResult); + expect(actionResult[2].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[2].type).toStrictEqual( + expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); }); @@ -1407,12 +1424,13 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(3); - expect(actionResult[0].type).toStrictEqual('send/updateSendAmount'); - expect(actionResult[1].type).toStrictEqual( + expect(actionResult).toHaveLength(4); + expect(actionResult[0].type).toStrictEqual('send/addHistoryEntry'); + expect(actionResult[1].type).toStrictEqual('send/updateSendAmount'); + expect(actionResult[2].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[2].type).toStrictEqual( + expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); }); @@ -1466,19 +1484,31 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(3); + expect(actionResult).toHaveLength(6); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset type to ', + }); + expect(actionResult[1]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset symbol to ', + }); + expect(actionResult[2]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset address to ', + }); - expect(actionResult[0].type).toStrictEqual('send/updateAsset'); - expect(actionResult[0].payload).toStrictEqual({ + expect(actionResult[3].type).toStrictEqual('send/updateAsset'); + expect(actionResult[3].payload).toStrictEqual({ ...newSendAsset, balance: '', error: null, }); - expect(actionResult[1].type).toStrictEqual( + expect(actionResult[4].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[2].type).toStrictEqual( + expect(actionResult[5].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); }); @@ -1506,19 +1536,31 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(5); - expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); - expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION'); - expect(actionResult[2].payload).toStrictEqual({ + expect(actionResult).toHaveLength(8); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: `sendFlow - user set asset type to ${ASSET_TYPES.TOKEN}`, + }); + expect(actionResult[1]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset symbol to tokenSymbol', + }); + expect(actionResult[2]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset address to tokenAddress', + }); + expect(actionResult[3].type).toStrictEqual('SHOW_LOADING_INDICATION'); + expect(actionResult[4].type).toStrictEqual('HIDE_LOADING_INDICATION'); + expect(actionResult[5].payload).toStrictEqual({ ...newSendAsset, balance: '0x0', error: null, }); - expect(actionResult[3].type).toStrictEqual( + expect(actionResult[6].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[4].type).toStrictEqual( + expect(actionResult[7].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); }); @@ -1543,10 +1585,22 @@ describe('Send Slice', () => { 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({ + expect(actionResult).toHaveLength(6); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: `sendFlow - user set asset type to ${ASSET_TYPES.TOKEN}`, + }); + expect(actionResult[1]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset symbol to tokenSymbol', + }); + expect(actionResult[2]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset address to tokenAddress', + }); + expect(actionResult[3].type).toStrictEqual('SHOW_LOADING_INDICATION'); + expect(actionResult[4].type).toStrictEqual('HIDE_LOADING_INDICATION'); + expect(actionResult[5]).toStrictEqual({ payload: { name: 'CONVERT_TOKEN_TO_NFT', tokenAddress: 'tokenAddress', @@ -1600,24 +1654,32 @@ describe('Send Slice', () => { await store.dispatch(updateRecipientUserInput(newUserRecipientInput)); - expect(store.getActions()).toHaveLength(1); - expect(store.getActions()[0].type).toStrictEqual( + const actionResult = store.getActions(); + + expect(actionResult).toHaveLength(1); + expect(actionResult[0].type).toStrictEqual( 'send/updateRecipientUserInput', ); - expect(store.getActions()[0].payload).toStrictEqual( - newUserRecipientInput, - ); + expect(actionResult[0].payload).toStrictEqual(newUserRecipientInput); clock.tick(300); // debounce - expect(store.getActions()).toHaveLength(2); - expect(store.getActions()[1].type).toStrictEqual( + const actionResultAfterDebounce = store.getActions(); + expect(actionResultAfterDebounce).toHaveLength(3); + + expect(actionResultAfterDebounce[1]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: `sendFlow - user typed ${newUserRecipientInput} into recipient input field`, + }); + + expect(actionResultAfterDebounce[2].type).toStrictEqual( 'send/validateRecipientUserInput', ); - expect(store.getActions()[1].payload).toStrictEqual({ + expect(actionResultAfterDebounce[2].payload).toStrictEqual({ chainId: '', tokens: [], useTokenDetection: true, + userInput: newUserRecipientInput, tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'], }); }); @@ -1630,8 +1692,13 @@ describe('Send Slice', () => { await store.dispatch(useContactListForRecipientSearch()); const actionResult = store.getActions(); + expect(actionResult).toHaveLength(2); expect(actionResult).toStrictEqual([ + { + type: 'send/addHistoryEntry', + payload: 'sendFlow - user selected back to all on recipient screen', + }, { type: 'send/updateRecipientSearchMode', payload: RECIPIENT_SEARCH_MODES.CONTACT_LIST, @@ -1648,7 +1715,14 @@ describe('Send Slice', () => { const actionResult = store.getActions(); + expect(actionResult).toHaveLength(2); + expect(actionResult).toStrictEqual([ + { + type: 'send/addHistoryEntry', + payload: + 'sendFlow - user selected transfer to my accounts on recipient screen', + }, { type: 'send/updateRecipientSearchMode', payload: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS, @@ -1890,20 +1964,24 @@ describe('Send Slice', () => { await store.dispatch(resetRecipientInput()); const actionResult = store.getActions(); - expect(actionResult).toHaveLength(6); - expect(actionResult[0].type).toStrictEqual( + expect(actionResult).toHaveLength(7); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user cleared recipient input', + }); + expect(actionResult[1].type).toStrictEqual( 'send/updateRecipientUserInput', ); - expect(actionResult[0].payload).toStrictEqual(''); - expect(actionResult[1].type).toStrictEqual('send/updateRecipient'); - expect(actionResult[2].type).toStrictEqual( + expect(actionResult[1].payload).toStrictEqual(''); + expect(actionResult[2].type).toStrictEqual('send/updateRecipient'); + expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[3].type).toStrictEqual( + expect(actionResult[4].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); - expect(actionResult[4].type).toStrictEqual('ENS/resetEnsResolution'); - expect(actionResult[5].type).toStrictEqual( + expect(actionResult[5].type).toStrictEqual('ENS/resetEnsResolution'); + expect(actionResult[6].type).toStrictEqual( 'send/validateRecipientUserInput', ); }); @@ -1927,10 +2005,14 @@ describe('Send Slice', () => { const actionResult = store.getActions(); const expectActionResult = [ + { + type: 'send/addHistoryEntry', + payload: 'sendFlow - user added custom hexData 0x1', + }, { type: 'send/updateUserInputHexData', payload: hexData }, ]; - expect(actionResult).toHaveLength(1); + expect(actionResult).toHaveLength(2); expect(actionResult).toStrictEqual(expectActionResult); }); }); @@ -1970,13 +2052,17 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(4); + expect(actionResult).toHaveLength(5); expect(actionResult[0].type).toStrictEqual('send/updateAmountMode'); expect(actionResult[1].type).toStrictEqual('send/updateAmountToMax'); - expect(actionResult[2].type).toStrictEqual( + expect(actionResult[2]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user toggled max mode on', + }); + expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[3].type).toStrictEqual( + expect(actionResult[4].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); }); @@ -2014,13 +2100,17 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(4); + expect(actionResult).toHaveLength(5); expect(actionResult[0].type).toStrictEqual('send/updateAmountMode'); expect(actionResult[1].type).toStrictEqual('send/updateSendAmount'); - expect(actionResult[2].type).toStrictEqual( + expect(actionResult[2]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user toggled max mode off', + }); + expect(actionResult[3].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[3].type).toStrictEqual( + expect(actionResult[4].type).toStrictEqual( 'send/computeEstimatedGasLimit/rejected', ); }); @@ -2045,8 +2135,13 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(1); - expect(actionResult[0].type).toStrictEqual('SHOW_CONF_TX_PAGE'); + expect(actionResult).toHaveLength(2); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: + 'sendFlow - user clicked next and transaction should be added to controller', + }); + expect(actionResult[1].type).toStrictEqual('SHOW_CONF_TX_PAGE'); }); it('should create actions for updateTransaction rejecting', async () => { @@ -2081,11 +2176,16 @@ describe('Send Slice', () => { const actionResult = store.getActions(); - expect(actionResult).toHaveLength(2); - expect(actionResult[0].type).toStrictEqual( + expect(actionResult).toHaveLength(3); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: + 'sendFlow - user clicked next and transaction should be updated in controller', + }); + expect(actionResult[1].type).toStrictEqual( 'UPDATE_TRANSACTION_EDITABLE_PARAMS', ); - expect(actionResult[1].type).toStrictEqual( + expect(actionResult[2].type).toStrictEqual( 'UPDATE_TRANSACTION_GAS_FEES', ); }); @@ -2133,9 +2233,13 @@ describe('Send Slice', () => { 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({ + expect(actionResult).toHaveLength(2); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user clicked edit on transaction with id 1', + }); + expect(actionResult[1].type).toStrictEqual('send/editTransaction'); + expect(actionResult[1].payload).toStrictEqual({ address: '0xRecipientAddress', amount: '0xde0b6b3a7640000', data: '', @@ -2146,7 +2250,7 @@ describe('Send Slice', () => { nickname: '', }); - const action = actionResult[0]; + const action = actionResult[1]; const result = sendReducer(initialState, action); @@ -2254,9 +2358,25 @@ describe('Send Slice', () => { ), ); const actionResult = store.getActions(); - expect(actionResult).toHaveLength(5); - expect(actionResult[0].type).toStrictEqual('send/updateAsset'); - expect(actionResult[0].payload).toStrictEqual({ + expect(actionResult).toHaveLength(9); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user clicked edit on transaction with id 1', + }); + expect(actionResult[1]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: `sendFlow - user set asset type to ${ASSET_TYPES.COLLECTIBLE}`, + }); + expect(actionResult[2]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset symbol to undefined', + }); + expect(actionResult[3]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset address to 0xTokenAddress', + }); + expect(actionResult[4].type).toStrictEqual('send/updateAsset'); + expect(actionResult[4].payload).toStrictEqual({ balance: '0x1', type: ASSET_TYPES.COLLECTIBLE, error: null, @@ -2270,18 +2390,17 @@ describe('Send Slice', () => { tokenId: '26847', }, }); - expect(actionResult[1].type).toStrictEqual( + expect(actionResult[5].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[2].type).toStrictEqual( + expect(actionResult[6].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); - expect(actionResult[3].type).toStrictEqual( + expect(actionResult[7].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); - expect(actionResult[4].type).toStrictEqual('send/editTransaction'); - - const action = actionResult[4]; + expect(actionResult[8].type).toStrictEqual('send/editTransaction'); + const action = actionResult[8]; const result = sendReducer(initialState, action); @@ -2383,11 +2502,27 @@ describe('Send Slice', () => { ); 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({ + expect(actionResult).toHaveLength(11); + expect(actionResult[0]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user clicked edit on transaction with id 1', + }); + expect(actionResult[1]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: `sendFlow - user set asset type to ${ASSET_TYPES.TOKEN}`, + }); + expect(actionResult[2]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset symbol to SYMB', + }); + expect(actionResult[3]).toMatchObject({ + type: 'send/addHistoryEntry', + payload: 'sendFlow - user set asset address to 0xTokenAddress', + }); + expect(actionResult[4].type).toStrictEqual('SHOW_LOADING_INDICATION'); + expect(actionResult[5].type).toStrictEqual('HIDE_LOADING_INDICATION'); + expect(actionResult[6].type).toStrictEqual('send/updateAsset'); + expect(actionResult[6].payload).toStrictEqual({ balance: '0x0', type: ASSET_TYPES.TOKEN, error: null, @@ -2398,17 +2533,17 @@ describe('Send Slice', () => { standard: 'ERC20', }, }); - expect(actionResult[3].type).toStrictEqual( + expect(actionResult[7].type).toStrictEqual( 'send/computeEstimatedGasLimit/pending', ); - expect(actionResult[4].type).toStrictEqual( + expect(actionResult[8].type).toStrictEqual( 'metamask/gas/SET_CUSTOM_GAS_LIMIT', ); - expect(actionResult[5].type).toStrictEqual( + expect(actionResult[9].type).toStrictEqual( 'send/computeEstimatedGasLimit/fulfilled', ); - expect(actionResult[6].type).toStrictEqual('send/editTransaction'); - expect(actionResult[6].payload).toStrictEqual({ + expect(actionResult[10].type).toStrictEqual('send/editTransaction'); + expect(actionResult[10].payload).toStrictEqual({ address: '0xrecipientaddress', // getting address from tokenData does .toLowerCase amount: '0x3a98', data: '', @@ -2419,7 +2554,7 @@ describe('Send Slice', () => { nickname: '', }); - const action = actionResult[6]; + const action = actionResult[10]; const result = sendReducer(initialState, action); diff --git a/ui/pages/send/send-content/add-recipient/add-recipient.component.js b/ui/pages/send/send-content/add-recipient/add-recipient.component.js index 100d2877f..a46fa7a8b 100644 --- a/ui/pages/send/send-content/add-recipient/add-recipient.component.js +++ b/ui/pages/send/send-content/add-recipient/add-recipient.component.js @@ -22,6 +22,7 @@ export default class AddRecipient extends Component { addressBookEntryName: PropTypes.string, contacts: PropTypes.array, nonContacts: PropTypes.array, + addHistoryEntry: PropTypes.func, useMyAccountsForRecipientSearch: PropTypes.func, useContactListForRecipientSearch: PropTypes.func, isUsingMyAccountsForRecipientSearch: PropTypes.bool, @@ -64,7 +65,10 @@ export default class AddRecipient extends Component { metricsEvent: PropTypes.func, }; - selectRecipient = (address, nickname = '') => { + selectRecipient = (address, nickname = '', type = 'user input') => { + this.props.addHistoryEntry( + `sendFlow - User clicked recipient from ${type}. address: ${address}, nickname ${nickname}`, + ); this.props.updateRecipient({ address, nickname }); }; @@ -109,11 +113,13 @@ export default class AddRecipient extends Component { content = this.renderExplicitAddress( recipient.address, recipient.nickname, + 'validated user input', ); } else if (ensResolution) { content = this.renderExplicitAddress( ensResolution, addressBookEntryName || userInput, + 'ENS resolution', ); } else if (isUsingMyAccountsForRecipientSearch) { content = this.renderTransfer(); @@ -127,12 +133,12 @@ export default class AddRecipient extends Component { ); } - renderExplicitAddress(address, name) { + renderExplicitAddress(address, name, type) { return (
this.selectRecipient(address, name)} + onClick={() => this.selectRecipient(address, name, type)} >
@@ -179,7 +185,9 @@ export default class AddRecipient extends Component { + this.selectRecipient(address, name, 'my accounts') + } />
); @@ -200,7 +208,9 @@ export default class AddRecipient extends Component { addressBook={addressBook} searchForContacts={this.searchForContacts.bind(this)} searchForRecents={this.searchForRecents.bind(this)} - selectRecipient={this.selectRecipient.bind(this)} + selectRecipient={(address, name) => + this.selectRecipient(address, name, 'contact list') + } > {ownedAccounts && ownedAccounts.length > 1 && !userInput && (