1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-23 02:10:12 +01:00

Track send flow history on txMeta (#14510)

This commit is contained in:
Brad Decker 2022-05-04 11:54:46 -05:00 committed by ryanml
parent 214211f847
commit f251ca4ff2
9 changed files with 415 additions and 91 deletions

View File

@ -647,6 +647,35 @@ export default class TransactionController extends EventEmitter {
return this._getTransaction(txId); 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 txParams
* @param origin * @param origin
* @param transactionType * @param transactionType
* @param sendFlowHistory
* @returns {txMeta} * @returns {txMeta}
*/ */
async addUnapprovedTransaction(txParams, origin, transactionType) { async addUnapprovedTransaction(
txParams,
origin,
transactionType,
sendFlowHistory = [],
) {
if ( if (
transactionType !== undefined && transactionType !== undefined &&
!VALID_UNAPPROVED_TRANSACTION_TYPES.includes(transactionType) !VALID_UNAPPROVED_TRANSACTION_TYPES.includes(transactionType)
@ -683,6 +718,7 @@ export default class TransactionController extends EventEmitter {
let txMeta = this.txStateManager.generateTxMeta({ let txMeta = this.txStateManager.generateTxMeta({
txParams: normalizedTxParams, txParams: normalizedTxParams,
origin, origin,
sendFlowHistory,
}); });
if (origin === ORIGIN_METAMASK) { if (origin === ORIGIN_METAMASK) {

View File

@ -127,6 +127,7 @@ export default class TransactionStateManager extends EventEmitter {
chainId, chainId,
loadingDefaults: true, loadingDefaults: true,
dappSuggestedGasFees, dappSuggestedGasFees,
sendFlowHistory: [],
...opts, ...opts,
}; };
} }

View File

@ -1632,6 +1632,9 @@ export default class MetamaskController extends EventEmitter {
updateTransactionGasFees: txController.updateTransactionGasFees.bind( updateTransactionGasFees: txController.updateTransactionGasFees.bind(
txController, txController,
), ),
updateTransactionSendFlowHistory: txController.updateTransactionSendFlowHistory.bind(
txController,
),
updateSwapApprovalTransaction: txController.updateSwapApprovalTransaction.bind( updateSwapApprovalTransaction: txController.updateSwapApprovalTransaction.bind(
txController, txController,

View File

@ -60,6 +60,7 @@ import {
getTokenStandardAndDetails, getTokenStandardAndDetails,
showModal, showModal,
addUnapprovedTransactionAndRouteToConfirmationPage, addUnapprovedTransactionAndRouteToConfirmationPage,
updateTransactionSendFlowHistory,
} from '../../store/actions'; } from '../../store/actions';
import { setCustomGasLimit } from '../gas/gas.duck'; import { setCustomGasLimit } from '../gas/gas.duck';
import { import {
@ -110,6 +111,7 @@ import {
import { readAddressAsContract } from '../../../shared/modules/contract-utils'; import { readAddressAsContract } from '../../../shared/modules/contract-utils';
import { INVALID_ASSET_TYPE } from '../../helpers/constants/error-keys'; import { INVALID_ASSET_TYPE } from '../../helpers/constants/error-keys';
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils'; import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
import { getValueFromWeiHex } from '../../helpers/utils/confirm-tx.util';
// typedefs // typedefs
/** /**
* @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction * @typedef {import('@reduxjs/toolkit').PayloadAction} PayloadAction
@ -684,12 +686,19 @@ export const initialState = {
// Layer 1 gas fee total on multi-layer fee networks // Layer 1 gas fee total on multi-layer fee networks
layer1GasTotal: '0x0', layer1GasTotal: '0x0',
}, },
history: [],
}; };
const slice = createSlice({ const slice = createSlice({
name, name,
initialState, initialState,
reducers: { 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 * update current amount.value in state and run post update validation of
* the amount field and the send state. Recomputes the draftTransaction * the amount field and the send state. Recomputes the draftTransaction
@ -1402,9 +1411,10 @@ const {
updateGasLimit, updateGasLimit,
validateRecipientUserInput, validateRecipientUserInput,
updateRecipientSearchMode, updateRecipientSearchMode,
addHistoryEntry,
} = actions; } = actions;
export { useDefaultGas, useCustomGas, updateGasLimit }; export { useDefaultGas, useCustomGas, updateGasLimit, addHistoryEntry };
// Action Creators // Action Creators
@ -1421,6 +1431,9 @@ export { useDefaultGas, useCustomGas, updateGasLimit };
*/ */
export function updateGasPrice(gasPrice) { export function updateGasPrice(gasPrice) {
return (dispatch) => { return (dispatch) => {
dispatch(
addHistoryEntry(`sendFlow - user set legacy gasPrice to ${gasPrice}`),
);
dispatch( dispatch(
actions.updateGasFees({ actions.updateGasFees({
gasPrice, gasPrice,
@ -1452,8 +1465,36 @@ export function resetSendState() {
*/ */
export function updateSendAmount(amount) { export function updateSendAmount(amount) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
await dispatch(actions.updateSendAmount(amount));
const state = getState(); 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) { if (state.send.amount.mode === AMOUNT_MODES.MAX) {
await dispatch(actions.updateAmountMode(AMOUNT_MODES.INPUT)); await dispatch(actions.updateAmountMode(AMOUNT_MODES.INPUT));
} }
@ -1482,6 +1523,19 @@ export function updateSendAmount(amount) {
*/ */
export function updateSendAsset({ type, details }) { export function updateSendAsset({ type, details }) {
return async (dispatch, getState) => { 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(); const state = getState();
let { balance, error } = state.send.asset; let { balance, error } = state.send.asset;
const userAddress = state.send.account.address ?? getSelectedAddress(state); 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. * it only applicable for use within action creators.
*/ */
const debouncedValidateRecipientUserInput = debounce((dispatch, payload) => { const debouncedValidateRecipientUserInput = debounce((dispatch, payload) => {
dispatch(
addHistoryEntry(
`sendFlow - user typed ${payload.userInput} into recipient input field`,
),
);
dispatch(validateRecipientUserInput(payload)); dispatch(validateRecipientUserInput(payload));
}, 300); }, 300);
@ -1600,6 +1659,7 @@ export function updateRecipientUserInput(userInput) {
const useTokenDetection = getUseTokenDetection(state); const useTokenDetection = getUseTokenDetection(state);
const tokenAddressList = Object.keys(getTokenList(state)); const tokenAddressList = Object.keys(getTokenList(state));
debouncedValidateRecipientUserInput(dispatch, { debouncedValidateRecipientUserInput(dispatch, {
userInput,
chainId, chainId,
tokens, tokens,
useTokenDetection, useTokenDetection,
@ -1610,12 +1670,22 @@ export function updateRecipientUserInput(userInput) {
export function useContactListForRecipientSearch() { export function useContactListForRecipientSearch() {
return (dispatch) => { return (dispatch) => {
dispatch(
addHistoryEntry(
`sendFlow - user selected back to all on recipient screen`,
),
);
dispatch(updateRecipientSearchMode(RECIPIENT_SEARCH_MODES.CONTACT_LIST)); dispatch(updateRecipientSearchMode(RECIPIENT_SEARCH_MODES.CONTACT_LIST));
}; };
} }
export function useMyAccountsForRecipientSearch() { export function useMyAccountsForRecipientSearch() {
return (dispatch) => { return (dispatch) => {
dispatch(
addHistoryEntry(
`sendFlow - user selected transfer to my accounts on recipient screen`,
),
);
dispatch(updateRecipientSearchMode(RECIPIENT_SEARCH_MODES.MY_ACCOUNTS)); dispatch(updateRecipientSearchMode(RECIPIENT_SEARCH_MODES.MY_ACCOUNTS));
}; };
} }
@ -1638,6 +1708,8 @@ export function useMyAccountsForRecipientSearch() {
*/ */
export function updateRecipient({ address, nickname }) { export function updateRecipient({ address, nickname }) {
return async (dispatch, getState) => { 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 state = getState();
const nicknameFromAddressBookEntryOrAccountName = const nicknameFromAddressBookEntryOrAccountName =
getAddressBookEntryOrAccountName(state, address) ?? ''; getAddressBookEntryOrAccountName(state, address) ?? '';
@ -1656,6 +1728,7 @@ export function updateRecipient({ address, nickname }) {
*/ */
export function resetRecipientInput() { export function resetRecipientInput() {
return async (dispatch) => { return async (dispatch) => {
await dispatch(addHistoryEntry(`sendFlow - user cleared recipient input`));
await dispatch(updateRecipientUserInput('')); await dispatch(updateRecipientUserInput(''));
await dispatch(updateRecipient({ address: '', nickname: '' })); await dispatch(updateRecipient({ address: '', nickname: '' }));
await dispatch(resetEnsResolution()); await dispatch(resetEnsResolution());
@ -1675,6 +1748,9 @@ export function resetRecipientInput() {
*/ */
export function updateSendHexData(hexData) { export function updateSendHexData(hexData) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
await dispatch(
addHistoryEntry(`sendFlow - user added custom hexData ${hexData}`),
);
await dispatch(actions.updateUserInputHexData(hexData)); await dispatch(actions.updateUserInputHexData(hexData));
const state = getState(); const state = getState();
if (state.send.asset.type === ASSET_TYPES.NATIVE) { if (state.send.asset.type === ASSET_TYPES.NATIVE) {
@ -1695,9 +1771,11 @@ export function toggleSendMaxMode() {
if (state.send.amount.mode === AMOUNT_MODES.MAX) { if (state.send.amount.mode === AMOUNT_MODES.MAX) {
await dispatch(actions.updateAmountMode(AMOUNT_MODES.INPUT)); await dispatch(actions.updateAmountMode(AMOUNT_MODES.INPUT));
await dispatch(actions.updateSendAmount('0x0')); await dispatch(actions.updateSendAmount('0x0'));
await dispatch(addHistoryEntry(`sendFlow - user toggled max mode off`));
} else { } else {
await dispatch(actions.updateAmountMode(AMOUNT_MODES.MAX)); await dispatch(actions.updateAmountMode(AMOUNT_MODES.MAX));
await dispatch(actions.updateAmountToMax()); await dispatch(actions.updateAmountToMax());
await dispatch(addHistoryEntry(`sendFlow - user toggled max mode on`));
} }
await dispatch(computeEstimatedGasLimit()); await dispatch(computeEstimatedGasLimit());
}; };
@ -1746,6 +1824,12 @@ export function signTransaction() {
eip1559support ? eip1559OnlyTxParamsToUpdate : txParams, 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(updateEditableParams(id, editingTx.txParams));
dispatch(updateTransactionGasFees(id, editingTx.txParams)); dispatch(updateTransactionGasFees(id, editingTx.txParams));
} else { } else {
@ -1757,10 +1841,17 @@ export function signTransaction() {
? TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM ? TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM
: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER; : TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER;
} }
await dispatch(
addHistoryEntry(
`sendFlow - user clicked next and transaction should be added to controller`,
),
);
dispatch( dispatch(
addUnapprovedTransactionAndRouteToConfirmationPage( addUnapprovedTransactionAndRouteToConfirmationPage(
txParams, txParams,
transactionType, transactionType,
state[name].history,
), ),
); );
} }
@ -1775,6 +1866,11 @@ export function editTransaction(
) { ) {
return async (dispatch, getState) => { return async (dispatch, getState) => {
const state = getState(); const state = getState();
await dispatch(
addHistoryEntry(
`sendFlow - user clicked edit on transaction with id ${transactionId}`,
),
);
const unapprovedTransactions = getUnapprovedTxs(state); const unapprovedTransactions = getUnapprovedTxs(state);
const transaction = unapprovedTransactions[transactionId]; const transaction = unapprovedTransactions[transactionId];
const { txParams } = transaction; const { txParams } = transaction;

View File

@ -80,9 +80,10 @@ jest.mock('./send', () => {
setBackgroundConnection({ setBackgroundConnection({
addPollingTokenToAppState: jest.fn(), addPollingTokenToAppState: jest.fn(),
addUnapprovedTransaction: jest.fn((_x, _y, _z, cb) => { addUnapprovedTransaction: jest.fn((_w, _x, _y, _z, cb) => {
return cb(null, {}); cb(null);
}), }),
updateTransactionSendFlowHistory: jest.fn((_x, _y, cb) => cb(null)),
}); });
describe('Send Slice', () => { describe('Send Slice', () => {
@ -1247,6 +1248,10 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
const expectedActionResult = [ const expectedActionResult = [
{
type: 'send/addHistoryEntry',
payload: 'sendFlow - user set legacy gasPrice to 0x0',
},
{ {
type: 'send/updateGasFees', type: 'send/updateGasFees',
payload: { payload: {
@ -1302,22 +1307,28 @@ describe('Send Slice', () => {
}; };
const store = mockStore(sendState); const store = mockStore(sendState);
const newSendAmount = 'aNewSendAmount'; const newSendAmount = 'DE0B6B3A7640000';
await store.dispatch(updateSendAmount(newSendAmount)); await store.dispatch(updateSendAmount(newSendAmount));
const actionResult = store.getActions(); const actionResult = store.getActions();
const expectedFirstActionResult = { const expectedFirstActionResult = {
type: 'send/addHistoryEntry',
payload: 'sendFlow - user set amount to 1 ETH',
};
const expectedSecondActionResult = {
type: 'send/updateSendAmount', type: 'send/updateSendAmount',
payload: 'aNewSendAmount', payload: 'DE0B6B3A7640000',
}; };
expect(actionResult[0]).toStrictEqual(expectedFirstActionResult); expect(actionResult[0]).toStrictEqual(expectedFirstActionResult);
expect(actionResult[1].type).toStrictEqual( expect(actionResult[1]).toStrictEqual(expectedSecondActionResult);
expect(actionResult[2].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[2].type).toStrictEqual( expect(actionResult[3].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected', 'send/computeEstimatedGasLimit/rejected',
); );
}); });
@ -1358,15 +1369,21 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
const expectedFirstActionResult = { const expectedFirstActionResult = {
type: 'send/addHistoryEntry',
payload: 'sendFlow - user set amount to 0 ETH',
};
const expectedSecondActionResult = {
type: 'send/updateSendAmount', type: 'send/updateSendAmount',
payload: undefined, payload: undefined,
}; };
expect(actionResult[0]).toStrictEqual(expectedFirstActionResult); expect(actionResult[0]).toStrictEqual(expectedFirstActionResult);
expect(actionResult[1].type).toStrictEqual( expect(actionResult[1]).toStrictEqual(expectedSecondActionResult);
expect(actionResult[2].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[2].type).toStrictEqual( expect(actionResult[3].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected', 'send/computeEstimatedGasLimit/rejected',
); );
}); });
@ -1407,12 +1424,13 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(3); expect(actionResult).toHaveLength(4);
expect(actionResult[0].type).toStrictEqual('send/updateSendAmount'); expect(actionResult[0].type).toStrictEqual('send/addHistoryEntry');
expect(actionResult[1].type).toStrictEqual( expect(actionResult[1].type).toStrictEqual('send/updateSendAmount');
expect(actionResult[2].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[2].type).toStrictEqual( expect(actionResult[3].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected', 'send/computeEstimatedGasLimit/rejected',
); );
}); });
@ -1466,19 +1484,31 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); 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[3].type).toStrictEqual('send/updateAsset');
expect(actionResult[0].payload).toStrictEqual({ expect(actionResult[3].payload).toStrictEqual({
...newSendAsset, ...newSendAsset,
balance: '', balance: '',
error: null, error: null,
}); });
expect(actionResult[1].type).toStrictEqual( expect(actionResult[4].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[2].type).toStrictEqual( expect(actionResult[5].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected', 'send/computeEstimatedGasLimit/rejected',
); );
}); });
@ -1506,19 +1536,31 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(5); expect(actionResult).toHaveLength(8);
expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); expect(actionResult[0]).toMatchObject({
expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION'); type: 'send/addHistoryEntry',
expect(actionResult[2].payload).toStrictEqual({ 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, ...newSendAsset,
balance: '0x0', balance: '0x0',
error: null, error: null,
}); });
expect(actionResult[3].type).toStrictEqual( expect(actionResult[6].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[4].type).toStrictEqual( expect(actionResult[7].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected', 'send/computeEstimatedGasLimit/rejected',
); );
}); });
@ -1543,10 +1585,22 @@ describe('Send Slice', () => {
store.dispatch(updateSendAsset(newSendAsset)), store.dispatch(updateSendAsset(newSendAsset)),
).rejects.toThrow('invalidAssetType'); ).rejects.toThrow('invalidAssetType');
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(3); expect(actionResult).toHaveLength(6);
expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); expect(actionResult[0]).toMatchObject({
expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION'); type: 'send/addHistoryEntry',
expect(actionResult[2]).toStrictEqual({ 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: { payload: {
name: 'CONVERT_TOKEN_TO_NFT', name: 'CONVERT_TOKEN_TO_NFT',
tokenAddress: 'tokenAddress', tokenAddress: 'tokenAddress',
@ -1600,24 +1654,32 @@ describe('Send Slice', () => {
await store.dispatch(updateRecipientUserInput(newUserRecipientInput)); await store.dispatch(updateRecipientUserInput(newUserRecipientInput));
expect(store.getActions()).toHaveLength(1); const actionResult = store.getActions();
expect(store.getActions()[0].type).toStrictEqual(
expect(actionResult).toHaveLength(1);
expect(actionResult[0].type).toStrictEqual(
'send/updateRecipientUserInput', 'send/updateRecipientUserInput',
); );
expect(store.getActions()[0].payload).toStrictEqual( expect(actionResult[0].payload).toStrictEqual(newUserRecipientInput);
newUserRecipientInput,
);
clock.tick(300); // debounce clock.tick(300); // debounce
expect(store.getActions()).toHaveLength(2); const actionResultAfterDebounce = store.getActions();
expect(store.getActions()[1].type).toStrictEqual( 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', 'send/validateRecipientUserInput',
); );
expect(store.getActions()[1].payload).toStrictEqual({ expect(actionResultAfterDebounce[2].payload).toStrictEqual({
chainId: '', chainId: '',
tokens: [], tokens: [],
useTokenDetection: true, useTokenDetection: true,
userInput: newUserRecipientInput,
tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'], tokenAddressList: ['0x514910771af9ca656af840dff83e8264ecf986ca'],
}); });
}); });
@ -1630,8 +1692,13 @@ describe('Send Slice', () => {
await store.dispatch(useContactListForRecipientSearch()); await store.dispatch(useContactListForRecipientSearch());
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(2);
expect(actionResult).toStrictEqual([ expect(actionResult).toStrictEqual([
{
type: 'send/addHistoryEntry',
payload: 'sendFlow - user selected back to all on recipient screen',
},
{ {
type: 'send/updateRecipientSearchMode', type: 'send/updateRecipientSearchMode',
payload: RECIPIENT_SEARCH_MODES.CONTACT_LIST, payload: RECIPIENT_SEARCH_MODES.CONTACT_LIST,
@ -1648,7 +1715,14 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(2);
expect(actionResult).toStrictEqual([ expect(actionResult).toStrictEqual([
{
type: 'send/addHistoryEntry',
payload:
'sendFlow - user selected transfer to my accounts on recipient screen',
},
{ {
type: 'send/updateRecipientSearchMode', type: 'send/updateRecipientSearchMode',
payload: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS, payload: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS,
@ -1890,20 +1964,24 @@ describe('Send Slice', () => {
await store.dispatch(resetRecipientInput()); await store.dispatch(resetRecipientInput());
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(6); expect(actionResult).toHaveLength(7);
expect(actionResult[0].type).toStrictEqual( expect(actionResult[0]).toMatchObject({
type: 'send/addHistoryEntry',
payload: 'sendFlow - user cleared recipient input',
});
expect(actionResult[1].type).toStrictEqual(
'send/updateRecipientUserInput', 'send/updateRecipientUserInput',
); );
expect(actionResult[0].payload).toStrictEqual(''); expect(actionResult[1].payload).toStrictEqual('');
expect(actionResult[1].type).toStrictEqual('send/updateRecipient'); expect(actionResult[2].type).toStrictEqual('send/updateRecipient');
expect(actionResult[2].type).toStrictEqual( expect(actionResult[3].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[3].type).toStrictEqual( expect(actionResult[4].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected', 'send/computeEstimatedGasLimit/rejected',
); );
expect(actionResult[4].type).toStrictEqual('ENS/resetEnsResolution'); expect(actionResult[5].type).toStrictEqual('ENS/resetEnsResolution');
expect(actionResult[5].type).toStrictEqual( expect(actionResult[6].type).toStrictEqual(
'send/validateRecipientUserInput', 'send/validateRecipientUserInput',
); );
}); });
@ -1927,10 +2005,14 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
const expectActionResult = [ const expectActionResult = [
{
type: 'send/addHistoryEntry',
payload: 'sendFlow - user added custom hexData 0x1',
},
{ type: 'send/updateUserInputHexData', payload: hexData }, { type: 'send/updateUserInputHexData', payload: hexData },
]; ];
expect(actionResult).toHaveLength(1); expect(actionResult).toHaveLength(2);
expect(actionResult).toStrictEqual(expectActionResult); expect(actionResult).toStrictEqual(expectActionResult);
}); });
}); });
@ -1970,13 +2052,17 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(4); expect(actionResult).toHaveLength(5);
expect(actionResult[0].type).toStrictEqual('send/updateAmountMode'); expect(actionResult[0].type).toStrictEqual('send/updateAmountMode');
expect(actionResult[1].type).toStrictEqual('send/updateAmountToMax'); 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', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[3].type).toStrictEqual( expect(actionResult[4].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected', 'send/computeEstimatedGasLimit/rejected',
); );
}); });
@ -2014,13 +2100,17 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(4); expect(actionResult).toHaveLength(5);
expect(actionResult[0].type).toStrictEqual('send/updateAmountMode'); expect(actionResult[0].type).toStrictEqual('send/updateAmountMode');
expect(actionResult[1].type).toStrictEqual('send/updateSendAmount'); 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', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[3].type).toStrictEqual( expect(actionResult[4].type).toStrictEqual(
'send/computeEstimatedGasLimit/rejected', 'send/computeEstimatedGasLimit/rejected',
); );
}); });
@ -2045,8 +2135,13 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(1); expect(actionResult).toHaveLength(2);
expect(actionResult[0].type).toStrictEqual('SHOW_CONF_TX_PAGE'); 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 () => { it('should create actions for updateTransaction rejecting', async () => {
@ -2081,11 +2176,16 @@ describe('Send Slice', () => {
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(2); expect(actionResult).toHaveLength(3);
expect(actionResult[0].type).toStrictEqual( 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', 'UPDATE_TRANSACTION_EDITABLE_PARAMS',
); );
expect(actionResult[1].type).toStrictEqual( expect(actionResult[2].type).toStrictEqual(
'UPDATE_TRANSACTION_GAS_FEES', 'UPDATE_TRANSACTION_GAS_FEES',
); );
}); });
@ -2133,9 +2233,13 @@ describe('Send Slice', () => {
await store.dispatch(editTransaction(ASSET_TYPES.NATIVE, 1)); await store.dispatch(editTransaction(ASSET_TYPES.NATIVE, 1));
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(1); expect(actionResult).toHaveLength(2);
expect(actionResult[0].type).toStrictEqual('send/editTransaction'); expect(actionResult[0]).toMatchObject({
expect(actionResult[0].payload).toStrictEqual({ 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', address: '0xRecipientAddress',
amount: '0xde0b6b3a7640000', amount: '0xde0b6b3a7640000',
data: '', data: '',
@ -2146,7 +2250,7 @@ describe('Send Slice', () => {
nickname: '', nickname: '',
}); });
const action = actionResult[0]; const action = actionResult[1];
const result = sendReducer(initialState, action); const result = sendReducer(initialState, action);
@ -2254,9 +2358,25 @@ describe('Send Slice', () => {
), ),
); );
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(5); expect(actionResult).toHaveLength(9);
expect(actionResult[0].type).toStrictEqual('send/updateAsset'); expect(actionResult[0]).toMatchObject({
expect(actionResult[0].payload).toStrictEqual({ 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', balance: '0x1',
type: ASSET_TYPES.COLLECTIBLE, type: ASSET_TYPES.COLLECTIBLE,
error: null, error: null,
@ -2270,18 +2390,17 @@ describe('Send Slice', () => {
tokenId: '26847', tokenId: '26847',
}, },
}); });
expect(actionResult[1].type).toStrictEqual( expect(actionResult[5].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[2].type).toStrictEqual( expect(actionResult[6].type).toStrictEqual(
'metamask/gas/SET_CUSTOM_GAS_LIMIT', 'metamask/gas/SET_CUSTOM_GAS_LIMIT',
); );
expect(actionResult[3].type).toStrictEqual( expect(actionResult[7].type).toStrictEqual(
'send/computeEstimatedGasLimit/fulfilled', 'send/computeEstimatedGasLimit/fulfilled',
); );
expect(actionResult[4].type).toStrictEqual('send/editTransaction'); expect(actionResult[8].type).toStrictEqual('send/editTransaction');
const action = actionResult[8];
const action = actionResult[4];
const result = sendReducer(initialState, action); const result = sendReducer(initialState, action);
@ -2383,11 +2502,27 @@ describe('Send Slice', () => {
); );
const actionResult = store.getActions(); const actionResult = store.getActions();
expect(actionResult).toHaveLength(7); expect(actionResult).toHaveLength(11);
expect(actionResult[0].type).toStrictEqual('SHOW_LOADING_INDICATION'); expect(actionResult[0]).toMatchObject({
expect(actionResult[1].type).toStrictEqual('HIDE_LOADING_INDICATION'); type: 'send/addHistoryEntry',
expect(actionResult[2].type).toStrictEqual('send/updateAsset'); payload: 'sendFlow - user clicked edit on transaction with id 1',
expect(actionResult[2].payload).toStrictEqual({ });
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', balance: '0x0',
type: ASSET_TYPES.TOKEN, type: ASSET_TYPES.TOKEN,
error: null, error: null,
@ -2398,17 +2533,17 @@ describe('Send Slice', () => {
standard: 'ERC20', standard: 'ERC20',
}, },
}); });
expect(actionResult[3].type).toStrictEqual( expect(actionResult[7].type).toStrictEqual(
'send/computeEstimatedGasLimit/pending', 'send/computeEstimatedGasLimit/pending',
); );
expect(actionResult[4].type).toStrictEqual( expect(actionResult[8].type).toStrictEqual(
'metamask/gas/SET_CUSTOM_GAS_LIMIT', 'metamask/gas/SET_CUSTOM_GAS_LIMIT',
); );
expect(actionResult[5].type).toStrictEqual( expect(actionResult[9].type).toStrictEqual(
'send/computeEstimatedGasLimit/fulfilled', 'send/computeEstimatedGasLimit/fulfilled',
); );
expect(actionResult[6].type).toStrictEqual('send/editTransaction'); expect(actionResult[10].type).toStrictEqual('send/editTransaction');
expect(actionResult[6].payload).toStrictEqual({ expect(actionResult[10].payload).toStrictEqual({
address: '0xrecipientaddress', // getting address from tokenData does .toLowerCase address: '0xrecipientaddress', // getting address from tokenData does .toLowerCase
amount: '0x3a98', amount: '0x3a98',
data: '', data: '',
@ -2419,7 +2554,7 @@ describe('Send Slice', () => {
nickname: '', nickname: '',
}); });
const action = actionResult[6]; const action = actionResult[10];
const result = sendReducer(initialState, action); const result = sendReducer(initialState, action);

View File

@ -22,6 +22,7 @@ export default class AddRecipient extends Component {
addressBookEntryName: PropTypes.string, addressBookEntryName: PropTypes.string,
contacts: PropTypes.array, contacts: PropTypes.array,
nonContacts: PropTypes.array, nonContacts: PropTypes.array,
addHistoryEntry: PropTypes.func,
useMyAccountsForRecipientSearch: PropTypes.func, useMyAccountsForRecipientSearch: PropTypes.func,
useContactListForRecipientSearch: PropTypes.func, useContactListForRecipientSearch: PropTypes.func,
isUsingMyAccountsForRecipientSearch: PropTypes.bool, isUsingMyAccountsForRecipientSearch: PropTypes.bool,
@ -64,7 +65,10 @@ export default class AddRecipient extends Component {
metricsEvent: PropTypes.func, 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 }); this.props.updateRecipient({ address, nickname });
}; };
@ -109,11 +113,13 @@ export default class AddRecipient extends Component {
content = this.renderExplicitAddress( content = this.renderExplicitAddress(
recipient.address, recipient.address,
recipient.nickname, recipient.nickname,
'validated user input',
); );
} else if (ensResolution) { } else if (ensResolution) {
content = this.renderExplicitAddress( content = this.renderExplicitAddress(
ensResolution, ensResolution,
addressBookEntryName || userInput, addressBookEntryName || userInput,
'ENS resolution',
); );
} else if (isUsingMyAccountsForRecipientSearch) { } else if (isUsingMyAccountsForRecipientSearch) {
content = this.renderTransfer(); content = this.renderTransfer();
@ -127,12 +133,12 @@ export default class AddRecipient extends Component {
); );
} }
renderExplicitAddress(address, name) { renderExplicitAddress(address, name, type) {
return ( return (
<div <div
key={address} key={address}
className="send__select-recipient-wrapper__group-item" className="send__select-recipient-wrapper__group-item"
onClick={() => this.selectRecipient(address, name)} onClick={() => this.selectRecipient(address, name, type)}
> >
<Identicon address={address} diameter={28} /> <Identicon address={address} diameter={28} />
<div className="send__select-recipient-wrapper__group-item__content"> <div className="send__select-recipient-wrapper__group-item__content">
@ -179,7 +185,9 @@ export default class AddRecipient extends Component {
<RecipientGroup <RecipientGroup
label={t('myAccounts')} label={t('myAccounts')}
items={ownedAccounts} items={ownedAccounts}
onSelect={this.selectRecipient} onSelect={(address, name) =>
this.selectRecipient(address, name, 'my accounts')
}
/> />
</div> </div>
); );
@ -200,7 +208,9 @@ export default class AddRecipient extends Component {
addressBook={addressBook} addressBook={addressBook}
searchForContacts={this.searchForContacts.bind(this)} searchForContacts={this.searchForContacts.bind(this)}
searchForRecents={this.searchForRecents.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 && ( {ownedAccounts && ownedAccounts.length > 1 && !userInput && (
<Button <Button

View File

@ -13,6 +13,7 @@ import {
getIsUsingMyAccountForRecipientSearch, getIsUsingMyAccountForRecipientSearch,
getRecipientUserInput, getRecipientUserInput,
getRecipient, getRecipient,
addHistoryEntry,
} from '../../../../ducks/send'; } from '../../../../ducks/send';
import { import {
getEnsResolution, getEnsResolution,
@ -55,6 +56,7 @@ function mapStateToProps(state) {
function mapDispatchToProps(dispatch) { function mapDispatchToProps(dispatch) {
return { return {
addHistoryEntry: (entry) => dispatch(addHistoryEntry(entry)),
updateRecipient: ({ address, nickname }) => updateRecipient: ({ address, nickname }) =>
dispatch(updateRecipient({ address, nickname })), dispatch(updateRecipient({ address, nickname })),
updateRecipientUserInput: (newInput) => updateRecipientUserInput: (newInput) =>

View File

@ -2,6 +2,7 @@ import React, { useEffect, useCallback, useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom'; import { useHistory, useLocation } from 'react-router-dom';
import { import {
addHistoryEntry,
getIsUsingMyAccountForRecipientSearch, getIsUsingMyAccountForRecipientSearch,
getRecipient, getRecipient,
getRecipientUserInput, getRecipientUserInput,
@ -95,13 +96,23 @@ export default function SendTransactionScreen() {
userInput={userInput} userInput={userInput}
className="send__to-row" className="send__to-row"
onChange={(address) => dispatch(updateRecipientUserInput(address))} onChange={(address) => dispatch(updateRecipientUserInput(address))}
onValidAddressTyped={(address) => onValidAddressTyped={(address) => {
dispatch(updateRecipient({ address, nickname: '' })) dispatch(
} addHistoryEntry(`sendFlow - Valid address typed ${address}`),
);
dispatch(updateRecipient({ address, nickname: '' }));
}}
internalSearch={isUsingMyAccountsForRecipientSearch} internalSearch={isUsingMyAccountsForRecipientSearch}
selectedAddress={recipient.address} selectedAddress={recipient.address}
selectedName={recipient.nickname} selectedName={recipient.nickname}
onPaste={(text) => updateRecipient({ address: text, nickname: '' })} onPaste={(text) => {
dispatch(
addHistoryEntry(
`sendFlow - User pasted ${text} into address field`,
),
);
return dispatch(updateRecipient({ address: text, nickname: '' }));
}}
onReset={() => dispatch(resetRecipientInput())} onReset={() => dispatch(resetRecipientInput())}
scanQrCode={() => { scanQrCode={() => {
trackEvent({ trackEvent({

View File

@ -738,6 +738,32 @@ export function updateEditableParams(txId, editableParams) {
}; };
} }
/**
* Appends new send flow history to a transaction
*
* @param {string} txId - the id of the transaction to update
* @param {Array<{event: string, timestamp: number}>} sendFlowHistory - the new send flow history to append to the
* transaction
* @returns {import('../../shared/constants/transaction').TransactionMeta}
*/
export function updateTransactionSendFlowHistory(txId, sendFlowHistory) {
return async (dispatch) => {
let updatedTransaction;
try {
updatedTransaction = await promisifiedBackground.updateTransactionSendFlowHistory(
txId,
sendFlowHistory,
);
} catch (error) {
dispatch(txError(error));
log.error(error.message);
throw error;
}
return updatedTransaction;
};
}
export function updateTransactionGasFees(txId, txGasFees) { export function updateTransactionGasFees(txId, txGasFees) {
return async (dispatch) => { return async (dispatch) => {
let updatedTransaction; let updatedTransaction;
@ -811,11 +837,14 @@ export function updateTransaction(txData, dontShowLoadingIndicator) {
* @param {import( * @param {import(
* '../../shared/constants/transaction' * '../../shared/constants/transaction'
* ).TransactionTypeString} type - The type of the transaction being added. * ).TransactionTypeString} type - The type of the transaction being added.
* @param {Array<{event: string, timestamp: number}>} sendFlowHistory - The
* history of the send flow at time of creation.
* @returns {import('../../shared/constants/transaction').TransactionMeta} * @returns {import('../../shared/constants/transaction').TransactionMeta}
*/ */
export function addUnapprovedTransactionAndRouteToConfirmationPage( export function addUnapprovedTransactionAndRouteToConfirmationPage(
txParams, txParams,
type, type,
sendFlowHistory,
) { ) {
return async (dispatch) => { return async (dispatch) => {
try { try {
@ -824,6 +853,7 @@ export function addUnapprovedTransactionAndRouteToConfirmationPage(
txParams, txParams,
ORIGIN_METAMASK, ORIGIN_METAMASK,
type, type,
sendFlowHistory,
); );
dispatch(showConfTxPage()); dispatch(showConfTxPage());
return txMeta; return txMeta;