1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 19:26:13 +02: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);
}
/**
* 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) {

View File

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

View File

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

View File

@ -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;

View File

@ -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);

View File

@ -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 (
<div
key={address}
className="send__select-recipient-wrapper__group-item"
onClick={() => this.selectRecipient(address, name)}
onClick={() => this.selectRecipient(address, name, type)}
>
<Identicon address={address} diameter={28} />
<div className="send__select-recipient-wrapper__group-item__content">
@ -179,7 +185,9 @@ export default class AddRecipient extends Component {
<RecipientGroup
label={t('myAccounts')}
items={ownedAccounts}
onSelect={this.selectRecipient}
onSelect={(address, name) =>
this.selectRecipient(address, name, 'my accounts')
}
/>
</div>
);
@ -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 && (
<Button

View File

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

View File

@ -2,6 +2,7 @@ import React, { useEffect, useCallback, useContext } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import {
addHistoryEntry,
getIsUsingMyAccountForRecipientSearch,
getRecipient,
getRecipientUserInput,
@ -95,13 +96,23 @@ export default function SendTransactionScreen() {
userInput={userInput}
className="send__to-row"
onChange={(address) => dispatch(updateRecipientUserInput(address))}
onValidAddressTyped={(address) =>
dispatch(updateRecipient({ address, nickname: '' }))
}
onValidAddressTyped={(address) => {
dispatch(
addHistoryEntry(`sendFlow - Valid address typed ${address}`),
);
dispatch(updateRecipient({ address, nickname: '' }));
}}
internalSearch={isUsingMyAccountsForRecipientSearch}
selectedAddress={recipient.address}
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())}
scanQrCode={() => {
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) {
return async (dispatch) => {
let updatedTransaction;
@ -811,11 +837,14 @@ export function updateTransaction(txData, dontShowLoadingIndicator) {
* @param {import(
* '../../shared/constants/transaction'
* ).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}
*/
export function addUnapprovedTransactionAndRouteToConfirmationPage(
txParams,
type,
sendFlowHistory,
) {
return async (dispatch) => {
try {
@ -824,6 +853,7 @@ export function addUnapprovedTransactionAndRouteToConfirmationPage(
txParams,
ORIGIN_METAMASK,
type,
sendFlowHistory,
);
dispatch(showConfTxPage());
return txMeta;