1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

feat: Refactor Transaction Confirmation selector (#18796)

This commit is contained in:
OGPoyraz 2023-05-11 07:56:17 +02:00 committed by GitHub
parent 033b529c17
commit d37d5bf0ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 229 additions and 116 deletions

View File

@ -713,7 +713,9 @@ export function setupController(
// User Interface setup // User Interface setup
// //
updateBadge(); controller.txController.initApprovals().then(() => {
updateBadge();
});
controller.txController.on( controller.txController.on(
METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE, METAMASK_CONTROLLER_EVENTS.UPDATE_BADGE,
updateBadge, updateBadge,

View File

@ -394,6 +394,22 @@ export default class TransactionController extends EventEmitter {
}); });
} }
/**
* Creates approvals for all unapproved transactions in the txStateManager.
*
* @returns {Promise<void>}
*/
async initApprovals() {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList();
return Promise.all(
Object.values(unapprovedTxs).map((txMeta) =>
this._requestApproval(txMeta, {
shouldShowRequest: false,
}),
),
);
}
// ==================================================================================================================================================== // ====================================================================================================================================================
/** /**
@ -2646,13 +2662,16 @@ export default class TransactionController extends EventEmitter {
); );
} }
_requestApproval(txMeta) { async _requestApproval(
txMeta,
{ shouldShowRequest } = { shouldShowRequest: true },
) {
const id = this._getApprovalId(txMeta); const id = this._getApprovalId(txMeta);
const { origin } = txMeta; const { origin } = txMeta;
const type = ApprovalType.Transaction; const type = ApprovalType.Transaction;
const requestData = { txId: txMeta.id }; const requestData = { txId: txMeta.id };
this.messagingSystem return this.messagingSystem
.call( .call(
'ApprovalController:addRequest', 'ApprovalController:addRequest',
{ {
@ -2661,7 +2680,7 @@ export default class TransactionController extends EventEmitter {
type, type,
requestData, requestData,
}, },
true, shouldShowRequest,
) )
.catch(() => { .catch(() => {
// Intentionally ignored as promise not currently used // Intentionally ignored as promise not currently used

View File

@ -2992,4 +2992,58 @@ describe('Transaction Controller', function () {
assert.equal(result.type, TransactionType.simpleSend); assert.equal(result.type, TransactionType.simpleSend);
}); });
}); });
describe('initApprovals', function () {
it('adds unapprovedTxs as approvals', async function () {
const firstTxId = '1';
txController.addTransaction(
{
id: firstTxId,
origin: ORIGIN_METAMASK,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
},
noop,
);
const secondTxId = '2';
txController.addTransaction(
{
id: secondTxId,
origin: ORIGIN_METAMASK,
status: TransactionStatus.unapproved,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
},
noop,
);
await txController.initApprovals();
assert.deepEqual(messengerMock.call.getCall(0).args, [
'ApprovalController:addRequest',
{
id: firstTxId,
origin: ORIGIN_METAMASK,
requestData: { txId: firstTxId },
type: ApprovalType.Transaction,
},
false,
]);
assert.deepEqual(messengerMock.call.getCall(1).args, [
'ApprovalController:addRequest',
{
id: secondTxId,
origin: ORIGIN_METAMASK,
requestData: { txId: secondTxId },
type: ApprovalType.Transaction,
},
false,
]);
});
});
}); });

View File

@ -2,7 +2,6 @@ import currencyFormatter from 'currency-formatter';
import currencies from 'currency-formatter/currencies'; import currencies from 'currency-formatter/currencies';
import { BigNumber } from 'bignumber.js'; import { BigNumber } from 'bignumber.js';
import { unconfirmedTransactionsCountSelector } from '../../selectors';
import { Numeric } from '../../../shared/modules/Numeric'; import { Numeric } from '../../../shared/modules/Numeric';
import { EtherDenomination } from '../../../shared/constants/common'; import { EtherDenomination } from '../../../shared/constants/common';
import { TransactionMeta } from '../../../shared/constants/transaction'; import { TransactionMeta } from '../../../shared/constants/transaction';
@ -86,22 +85,6 @@ export function convertTokenToFiat({
return tokenInFiat.round(2).toString(); return tokenInFiat.round(2).toString();
} }
/**
* This is a selector and probably doesn't belong here but its staying for now
* Note: I did not go so far as to type the entirety of the MetaMask state tree
* which definitely needs to be done for the full conversion of TypeScript to
* be successful and as useful as possible.
* TODO: Type the MetaMask state tree and use that type here.
*
* @param state - MetaMask state
* @returns true if there are unconfirmed transactions in state
*/
export function hasUnconfirmedTransactions(
state: Record<string, any>,
): boolean {
return unconfirmedTransactionsCountSelector(state) > 0;
}
/** /**
* Rounds the given decimal string to 4 significant digits. * Rounds the given decimal string to 4 significant digits.
* *

View File

@ -107,7 +107,7 @@ export default class Home extends PureComponent {
history: PropTypes.object, history: PropTypes.object,
forgottenPassword: PropTypes.bool, forgottenPassword: PropTypes.bool,
hasWatchAssetPendingApprovals: PropTypes.bool, hasWatchAssetPendingApprovals: PropTypes.bool,
unconfirmedTransactionsCount: PropTypes.number, hasTransactionPendingApprovals: PropTypes.bool.isRequired,
shouldShowSeedPhraseReminder: PropTypes.bool.isRequired, shouldShowSeedPhraseReminder: PropTypes.bool.isRequired,
isPopup: PropTypes.bool, isPopup: PropTypes.bool,
isNotification: PropTypes.bool.isRequired, isNotification: PropTypes.bool.isRequired,
@ -197,7 +197,7 @@ export default class Home extends PureComponent {
showAwaitingSwapScreen, showAwaitingSwapScreen,
hasWatchAssetPendingApprovals, hasWatchAssetPendingApprovals,
swapsFetchParams, swapsFetchParams,
unconfirmedTransactionsCount, hasTransactionPendingApprovals,
} = this.props; } = this.props;
if (shouldCloseNotificationPopup(props)) { if (shouldCloseNotificationPopup(props)) {
@ -205,7 +205,7 @@ export default class Home extends PureComponent {
closeNotificationPopup(); closeNotificationPopup();
} else if ( } else if (
firstPermissionsRequestId || firstPermissionsRequestId ||
unconfirmedTransactionsCount > 0 || hasTransactionPendingApprovals ||
hasWatchAssetPendingApprovals || hasWatchAssetPendingApprovals ||
(!isNotification && (!isNotification &&
(showAwaitingSwapScreen || haveSwapsQuotes || swapsFetchParams)) (showAwaitingSwapScreen || haveSwapsQuotes || swapsFetchParams))
@ -269,7 +269,7 @@ export default class Home extends PureComponent {
history, history,
isNotification, isNotification,
hasWatchAssetPendingApprovals, hasWatchAssetPendingApprovals,
unconfirmedTransactionsCount, hasTransactionPendingApprovals,
haveSwapsQuotes, haveSwapsQuotes,
showAwaitingSwapScreen, showAwaitingSwapScreen,
swapsFetchParams, swapsFetchParams,
@ -288,7 +288,7 @@ export default class Home extends PureComponent {
history.push(BUILD_QUOTE_ROUTE); history.push(BUILD_QUOTE_ROUTE);
} else if (firstPermissionsRequestId) { } else if (firstPermissionsRequestId) {
history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`); history.push(`${CONNECT_ROUTE}/${firstPermissionsRequestId}`);
} else if (unconfirmedTransactionsCount > 0) { } else if (hasTransactionPendingApprovals) {
history.push(CONFIRM_TRANSACTION_ROUTE); history.push(CONFIRM_TRANSACTION_ROUTE);
} else if (hasWatchAssetPendingApprovals) { } else if (hasWatchAssetPendingApprovals) {
history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE); history.push(CONFIRM_ADD_SUGGESTED_TOKEN_ROUTE);

View File

@ -22,7 +22,6 @@ import {
getTotalUnapprovedCount, getTotalUnapprovedCount,
getUnapprovedTemplatedConfirmations, getUnapprovedTemplatedConfirmations,
getWeb3ShimUsageStateForOrigin, getWeb3ShimUsageStateForOrigin,
unconfirmedTransactionsCountSelector,
getInfuraBlocked, getInfuraBlocked,
getShowWhatsNewPopup, getShowWhatsNewPopup,
getSortedAnnouncementsToShow, getSortedAnnouncementsToShow,
@ -36,7 +35,7 @@ import {
getNewTokensImported, getNewTokensImported,
getShouldShowSeedPhraseReminder, getShouldShowSeedPhraseReminder,
getRemoveNftMessage, getRemoveNftMessage,
hasPendingApprovalsSelector, hasPendingApprovals,
} from '../../selectors'; } from '../../selectors';
import { import {
@ -71,6 +70,7 @@ import {
AlertTypes, AlertTypes,
Web3ShimUsageAlertStates, Web3ShimUsageAlertStates,
} from '../../../shared/constants/alerts'; } from '../../../shared/constants/alerts';
import { hasTransactionPendingApprovals } from '../../selectors/transactions';
import Home from './home.component'; import Home from './home.component';
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
@ -121,7 +121,7 @@ const mapStateToProps = (state) => {
hasUnsignedQRHardwareTransaction(state) || hasUnsignedQRHardwareTransaction(state) ||
hasUnsignedQRHardwareMessage(state); hasUnsignedQRHardwareMessage(state);
const hasWatchAssetPendingApprovals = hasPendingApprovalsSelector( const hasWatchAssetPendingApprovals = hasPendingApprovals(
state, state,
ApprovalType.WatchAsset, ApprovalType.WatchAsset,
); );
@ -130,7 +130,7 @@ const mapStateToProps = (state) => {
forgottenPassword, forgottenPassword,
hasWatchAssetPendingApprovals, hasWatchAssetPendingApprovals,
swapsEnabled, swapsEnabled,
unconfirmedTransactionsCount: unconfirmedTransactionsCountSelector(state), hasTransactionPendingApprovals: hasTransactionPendingApprovals(state),
shouldShowSeedPhraseReminder: getShouldShowSeedPhraseReminder(state), shouldShowSeedPhraseReminder: getShouldShowSeedPhraseReminder(state),
isPopup, isPopup,
isNotification, isNotification,

View File

@ -1,10 +1,10 @@
import { ApprovalType } from '@metamask/controller-utils'; import { ApprovalType } from '@metamask/controller-utils';
import { hasPendingApprovalsSelector } from './approvals'; import { hasPendingApprovals } from './approvals';
describe('approval selectors', () => { describe('approval selectors', () => {
const mockedState = { const mockedState = {
metamask: { metamask: {
pendingApprovalCount: 2, pendingApprovalCount: 3,
pendingApprovals: { pendingApprovals: {
'1': { '1': {
id: '1', id: '1',
@ -18,28 +18,30 @@ describe('approval selectors', () => {
id: '2', id: '2',
origin: 'origin', origin: 'origin',
time: Date.now(), time: Date.now(),
type: ApprovalType.EthSignTypedData, type: ApprovalType.Transaction,
requestData: {}, requestData: {},
requestState: null, requestState: null,
}, },
}, },
unapprovedTxs: {
'2': {
id: '2',
},
},
}, },
}; };
describe('hasPendingApprovalsSelector', () => { describe('hasPendingApprovals', () => {
it('should return true if there is a pending approval request', () => { it('should return true if there is a pending approval request', () => {
const result = hasPendingApprovalsSelector( const result = hasPendingApprovals(mockedState, ApprovalType.WatchAsset);
mockedState,
ApprovalType.WatchAsset,
);
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false if there is no pending approval request', () => { it('should return false if there is no pending approval request', () => {
const result = hasPendingApprovalsSelector( const result = hasPendingApprovals(
mockedState, mockedState,
ApprovalType.Transaction, ApprovalType.SnapDialogPrompt,
); );
expect(result).toBe(false); expect(result).toBe(false);

View File

@ -1,19 +1,35 @@
import { ApprovalControllerState } from '@metamask/approval-controller'; import { ApprovalControllerState } from '@metamask/approval-controller';
import { ApprovalType } from '@metamask/controller-utils'; import { ApprovalType } from '@metamask/controller-utils';
import { TransactionMeta } from '../../shared/constants/transaction';
type ApprovalsMetaMaskState = { type ApprovalsMetaMaskState = {
metamask: { metamask: {
pendingApprovals: ApprovalControllerState['pendingApprovals']; pendingApprovals: ApprovalControllerState['pendingApprovals'];
unapprovedTxs: {
[transactionId: string]: TransactionMeta;
};
}; };
}; };
export function hasPendingApprovalsSelector( export const getApprovalRequestsByType = (
state: ApprovalsMetaMaskState, state: ApprovalsMetaMaskState,
approvalType: ApprovalType, approvalType: ApprovalType,
) { ) => {
const pendingApprovalRequests = Object.values( const pendingApprovalRequests = Object.values(
state.metamask.pendingApprovals, state.metamask.pendingApprovals,
).filter(({ type }) => type === approvalType); ).filter(({ type }) => type === approvalType);
return pendingApprovalRequests;
};
export function hasPendingApprovals(
state: ApprovalsMetaMaskState,
approvalType: ApprovalType,
) {
const pendingApprovalRequests = getApprovalRequestsByType(
state,
approvalType,
);
return pendingApprovalRequests.length > 0; return pendingApprovalRequests.length > 0;
} }

View File

@ -142,50 +142,6 @@ export const unconfirmedMessagesHashSelector = createSelector(
}, },
); );
const unapprovedMsgCountSelector = (state) => state.metamask.unapprovedMsgCount;
const unapprovedPersonalMsgCountSelector = (state) =>
state.metamask.unapprovedPersonalMsgCount;
const unapprovedDecryptMsgCountSelector = (state) =>
state.metamask.unapprovedDecryptMsgCount;
const unapprovedEncryptionPublicKeyMsgCountSelector = (state) =>
state.metamask.unapprovedEncryptionPublicKeyMsgCount;
const unapprovedTypedMessagesCountSelector = (state) =>
state.metamask.unapprovedTypedMessagesCount;
export const unconfirmedTransactionsCountSelector = createSelector(
unapprovedTxsSelector,
unapprovedMsgCountSelector,
unapprovedPersonalMsgCountSelector,
unapprovedDecryptMsgCountSelector,
unapprovedEncryptionPublicKeyMsgCountSelector,
unapprovedTypedMessagesCountSelector,
deprecatedGetCurrentNetworkId,
getCurrentChainId,
(
unapprovedTxs = {},
unapprovedMsgCount = 0,
unapprovedPersonalMsgCount = 0,
unapprovedDecryptMsgCount = 0,
unapprovedEncryptionPublicKeyMsgCount = 0,
unapprovedTypedMessagesCount = 0,
network,
chainId,
) => {
const filteredUnapprovedTxIds = Object.keys(unapprovedTxs).filter((txId) =>
transactionMatchesNetwork(unapprovedTxs[txId], chainId, network),
);
return (
filteredUnapprovedTxIds.length +
unapprovedTypedMessagesCount +
unapprovedMsgCount +
unapprovedPersonalMsgCount +
unapprovedDecryptMsgCount +
unapprovedEncryptionPublicKeyMsgCount
);
},
);
export const currentCurrencySelector = (state) => export const currentCurrencySelector = (state) =>
state.metamask.currentCurrency; state.metamask.currentCurrency;
export const conversionRateSelector = (state) => state.metamask.conversionRate; export const conversionRateSelector = (state) => state.metamask.conversionRate;

View File

@ -1,7 +1,5 @@
import { CHAIN_IDS } from '../../shared/constants/network';
import { TransactionType } from '../../shared/constants/transaction'; import { TransactionType } from '../../shared/constants/transaction';
import { import {
unconfirmedTransactionsCountSelector,
sendTokenTokenAmountAndToAddressSelector, sendTokenTokenAmountAndToAddressSelector,
contractExchangeRateSelector, contractExchangeRateSelector,
conversionRateSelector, conversionRateSelector,
@ -17,32 +15,6 @@ const getEthersArrayLikeFromObj = (obj) => {
}; };
describe('Confirm Transaction Selector', () => { describe('Confirm Transaction Selector', () => {
describe('unconfirmedTransactionsCountSelector', () => {
const state = {
metamask: {
unapprovedTxs: {
1: {
metamaskNetworkId: '5',
},
2: {
chainId: CHAIN_IDS.MAINNET,
},
},
unapprovedMsgCount: 1,
unapprovedPersonalMsgCount: 1,
unapprovedTypedMessagesCount: 1,
networkId: '5',
providerConfig: {
chainId: '0x5',
},
},
};
it('returns number of txs in unapprovedTxs state with the same network plus unapproved signing method counts', () => {
expect(unconfirmedTransactionsCountSelector(state)).toStrictEqual(4);
});
});
describe('sendTokenTokenAmountAndToAddressSelector', () => { describe('sendTokenTokenAmountAndToAddressSelector', () => {
const state = { const state = {
confirmTransaction: { confirmTransaction: {

View File

@ -1,4 +1,5 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { ApprovalType } from '@metamask/controller-utils';
import { import {
PRIORITY_STATUS_HASH, PRIORITY_STATUS_HASH,
PENDING_STATUS_HASH, PENDING_STATUS_HASH,
@ -17,6 +18,7 @@ import {
deprecatedGetCurrentNetworkId, deprecatedGetCurrentNetworkId,
getSelectedAddress, getSelectedAddress,
} from './selectors'; } from './selectors';
import { hasPendingApprovals, getApprovalRequestsByType } from './approvals';
const INVALID_INITIAL_TRANSACTION_TYPES = [ const INVALID_INITIAL_TRANSACTION_TYPES = [
TransactionType.cancel, TransactionType.cancel,
@ -534,3 +536,34 @@ export const submittedPendingTransactionsSelector = createSelector(
(transaction) => transaction.status === TransactionStatus.submitted, (transaction) => transaction.status === TransactionStatus.submitted,
), ),
); );
const hasUnapprovedTransactionsInCurrentNetwork = (state) => {
const { unapprovedTxs } = state.metamask;
const unapprovedTxRequests = getApprovalRequestsByType(
state,
ApprovalType.Transaction,
);
const chainId = getCurrentChainId(state);
const filteredUnapprovedTxInCurrentNetwork = unapprovedTxRequests.filter(
({ id }) => transactionMatchesNetwork(unapprovedTxs[id], chainId),
);
return filteredUnapprovedTxInCurrentNetwork.length > 0;
};
const TRANSACTION_APPROVAL_TYPES = [
ApprovalType.EthDecrypt,
ApprovalType.EthGetEncryptionPublicKey,
ApprovalType.EthSign,
ApprovalType.EthSignTypedData,
ApprovalType.PersonalSign,
];
export function hasTransactionPendingApprovals(state) {
return (
hasUnapprovedTransactionsInCurrentNetwork(state) ||
TRANSACTION_APPROVAL_TYPES.some((type) => hasPendingApprovals(state, type))
);
}

View File

@ -1,3 +1,4 @@
import { ApprovalType } from '@metamask/controller-utils';
import { CHAIN_IDS } from '../../shared/constants/network'; import { CHAIN_IDS } from '../../shared/constants/network';
import { TransactionStatus } from '../../shared/constants/transaction'; import { TransactionStatus } from '../../shared/constants/transaction';
import { import {
@ -7,6 +8,7 @@ import {
nonceSortedPendingTransactionsSelector, nonceSortedPendingTransactionsSelector,
nonceSortedCompletedTransactionsSelector, nonceSortedCompletedTransactionsSelector,
submittedPendingTransactionsSelector, submittedPendingTransactionsSelector,
hasTransactionPendingApprovals,
} from './transactions'; } from './transactions';
describe('Transaction Selectors', () => { describe('Transaction Selectors', () => {
@ -329,4 +331,78 @@ describe('Transaction Selectors', () => {
); );
}); });
}); });
describe('hasTransactionPendingApprovals', () => {
const mockNetworkId = 'mockNetworkId';
const mockedState = {
metamask: {
providerConfig: {
chainId: mockNetworkId,
},
pendingApprovalCount: 2,
pendingApprovals: {
1: {
id: '1',
origin: 'origin',
time: Date.now(),
type: ApprovalType.WatchAsset,
requestData: {},
requestState: null,
},
2: {
id: '2',
origin: 'origin',
time: Date.now(),
type: ApprovalType.Transaction,
requestData: {},
requestState: null,
},
},
unapprovedTxs: {
2: {
id: '2',
chainId: mockNetworkId,
},
},
},
};
it('should return true if there is a pending transaction on same network', () => {
const result = hasTransactionPendingApprovals(mockedState);
expect(result).toBe(true);
});
it('should return false if there is a pending transaction on different network', () => {
mockedState.metamask.unapprovedTxs['2'].chainId = 'differentNetworkId';
const result = hasTransactionPendingApprovals(mockedState);
expect(result).toBe(false);
});
it.each([
[ApprovalType.EthDecrypt],
[ApprovalType.EthGetEncryptionPublicKey],
[ApprovalType.EthSign],
[ApprovalType.EthSignTypedData],
[ApprovalType.PersonalSign],
])(
'should return true if there is a pending transaction of %s type',
(type) => {
const result = hasTransactionPendingApprovals({
...mockedState,
metamask: {
...mockedState.metamask,
pendingApprovals: {
2: {
id: '2',
origin: 'origin',
time: Date.now(),
type,
requestData: {},
requestState: null,
},
},
},
});
expect(result).toBe(true);
},
);
});
}); });

View File

@ -23,12 +23,12 @@ import {
POLLING_TOKEN_ENVIRONMENT_TYPES, POLLING_TOKEN_ENVIRONMENT_TYPES,
MESSAGE_TYPE, MESSAGE_TYPE,
} from '../../shared/constants/app'; } from '../../shared/constants/app';
import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util';
import { getEnvironmentType, addHexPrefix } from '../../app/scripts/lib/util'; import { getEnvironmentType, addHexPrefix } from '../../app/scripts/lib/util';
import { import {
getMetaMaskAccounts, getMetaMaskAccounts,
getPermittedAccountsForCurrentTab, getPermittedAccountsForCurrentTab,
getSelectedAddress, getSelectedAddress,
hasTransactionPendingApprovals,
///: BEGIN:ONLY_INCLUDE_IN(snaps) ///: BEGIN:ONLY_INCLUDE_IN(snaps)
getNotifications, getNotifications,
///: END:ONLY_INCLUDE_IN ///: END:ONLY_INCLUDE_IN
@ -2698,7 +2698,7 @@ export function closeCurrentNotificationWindow(): ThunkAction<
return (_, getState) => { return (_, getState) => {
if ( if (
getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION && getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION &&
!hasUnconfirmedTransactions(getState()) !hasTransactionPendingApprovals(getState())
) { ) {
closeNotificationPopup(); closeNotificationPopup();
} }