From a0c4febfcef2368df6a2afb963e57b2b64a15116 Mon Sep 17 00:00:00 2001 From: dragana8 <92531782+dragana8@users.noreply.github.com> Date: Mon, 16 May 2022 20:36:19 +0200 Subject: [PATCH] "Cancel/reject all" for signature requests #13201 (#13786) --- .../app/signature-request-original/index.scss | 4 + .../signature-request-original.component.js | 36 ++++++++ .../signature-request-original.container.js | 28 +++++- ui/selectors/confirm-transaction.js | 23 +++++ ui/selectors/selectors.js | 18 ++++ ui/store/actions.js | 91 +++++++++++++++++++ ui/store/actions.test.js | 54 +++++++++++ 7 files changed, 251 insertions(+), 3 deletions(-) diff --git a/ui/components/app/signature-request-original/index.scss b/ui/components/app/signature-request-original/index.scss index 2e2aca65f..dd1e5e05c 100644 --- a/ui/components/app/signature-request-original/index.scss +++ b/ui/components/app/signature-request-original/index.scss @@ -20,6 +20,10 @@ @media screen and (min-width: $break-large) { height: 620px; } + + &__reject { + padding-bottom: 20px; + } } &__typed-container { diff --git a/ui/components/app/signature-request-original/signature-request-original.component.js b/ui/components/app/signature-request-original/signature-request-original.component.js index 80954ce93..a3eef4ec0 100644 --- a/ui/components/app/signature-request-original/signature-request-original.component.js +++ b/ui/components/app/signature-request-original/signature-request-original.component.js @@ -38,6 +38,9 @@ export default class SignatureRequestOriginal extends Component { hardwareWalletRequiresConnection: PropTypes.bool, isLedgerWallet: PropTypes.bool, nativeCurrency: PropTypes.string.isRequired, + messagesCount: PropTypes.number, + showRejectTransactionsConfirmationModal: PropTypes.func.isRequired, + cancelAll: PropTypes.func.isRequired, }; state = { @@ -315,7 +318,31 @@ export default class SignatureRequestOriginal extends Component { ); }; + handleCancelAll = () => { + const { + cancelAll, + clearConfirmTransaction, + history, + mostRecentOverviewPage, + showRejectTransactionsConfirmationModal, + messagesCount, + } = this.props; + const unapprovedTxCount = messagesCount; + + showRejectTransactionsConfirmationModal({ + unapprovedTxCount, + onSubmit: async () => { + await cancelAll(); + clearConfirmTransaction(); + history.push(mostRecentOverviewPage); + }, + }); + }; + render = () => { + const { messagesCount } = this.props; + const { t } = this.context; + const rejectNText = t('rejectTxsN', [messagesCount]); return (
{this.renderHeader()} @@ -326,6 +353,15 @@ export default class SignatureRequestOriginal extends Component {
) : null} {this.renderFooter()} + {messagesCount > 1 ? ( + + ) : null} ); }; diff --git a/ui/components/app/signature-request-original/signature-request-original.container.js b/ui/components/app/signature-request-original/signature-request-original.container.js index 8031d92ed..f30310358 100644 --- a/ui/components/app/signature-request-original/signature-request-original.container.js +++ b/ui/components/app/signature-request-original/signature-request-original.container.js @@ -3,14 +3,16 @@ import { compose } from 'redux'; import { withRouter } from 'react-router-dom'; import { MESSAGE_TYPE } from '../../../../shared/constants/app'; -import { goHome } from '../../../store/actions'; +import { goHome, cancelMsgs, showModal } from '../../../store/actions'; import { accountsWithSendEtherInfoSelector, conversionRateSelector, getSubjectMetadata, doesAddressRequireLedgerHidConnection, + unconfirmedMessagesHashSelector, + getTotalUnapprovedMessagesCount, } from '../../../selectors'; -import { getAccountByAddress } from '../../../helpers/utils/util'; +import { getAccountByAddress, valuesFor } from '../../../helpers/utils/util'; import { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck'; import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { @@ -29,6 +31,8 @@ function mapStateToProps(state, ownProps) { from, ); const isLedgerWallet = isAddressLedger(state, from); + const messagesList = unconfirmedMessagesHashSelector(state); + const messagesCount = getTotalUnapprovedMessagesCount(state); return { requester: null, @@ -41,6 +45,8 @@ function mapStateToProps(state, ownProps) { // not passed to component allAccounts: accountsWithSendEtherInfoSelector(state), subjectMetadata: getSubjectMetadata(state), + messagesList, + messagesCount, }; } @@ -48,6 +54,19 @@ function mapDispatchToProps(dispatch) { return { goHome: () => dispatch(goHome()), clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), + showRejectTransactionsConfirmationModal: ({ + onSubmit, + unapprovedTxCount: messagesCount, + }) => { + return dispatch( + showModal({ + name: 'REJECT_TRANSACTIONS', + onSubmit, + unapprovedTxCount: messagesCount, + }), + ); + }, + cancelAll: (messagesList) => dispatch(cancelMsgs(messagesList)), }; } @@ -62,7 +81,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) { txData, } = ownProps; - const { allAccounts, ...otherStateProps } = stateProps; + const { allAccounts, messagesList, ...otherStateProps } = stateProps; const { type, @@ -71,6 +90,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) { const fromAccount = getAccountByAddress(allAccounts, from); + const { cancelAll: dispatchCancelAll } = dispatchProps; + let cancel; let sign; if (type === MESSAGE_TYPE.PERSONAL_SIGN) { @@ -92,6 +113,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) { txData, cancel, sign, + cancelAll: () => dispatchCancelAll(valuesFor(messagesList)), }; } diff --git a/ui/selectors/confirm-transaction.js b/ui/selectors/confirm-transaction.js index e3e8f6efb..e502d473c 100644 --- a/ui/selectors/confirm-transaction.js +++ b/ui/selectors/confirm-transaction.js @@ -117,6 +117,29 @@ export const unconfirmedTransactionsHashSelector = createSelector( }, ); +export const unconfirmedMessagesHashSelector = createSelector( + unapprovedMsgsSelector, + unapprovedPersonalMsgsSelector, + unapprovedDecryptMsgsSelector, + unapprovedEncryptionPublicKeyMsgsSelector, + unapprovedTypedMessagesSelector, + ( + unapprovedMsgs = {}, + unapprovedPersonalMsgs = {}, + unapprovedDecryptMsgs = {}, + unapprovedEncryptionPublicKeyMsgs = {}, + unapprovedTypedMessages = {}, + ) => { + return { + ...unapprovedMsgs, + ...unapprovedPersonalMsgs, + ...unapprovedDecryptMsgs, + ...unapprovedEncryptionPublicKeyMsgs, + ...unapprovedTypedMessages, + }; + }, +); + const unapprovedMsgCountSelector = (state) => state.metamask.unapprovedMsgCount; const unapprovedPersonalMsgCountSelector = (state) => state.metamask.unapprovedPersonalMsgCount; diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 50ee18dbd..e1db5e2cf 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -470,6 +470,24 @@ export function getTotalUnapprovedCount(state) { ); } +export function getTotalUnapprovedMessagesCount(state) { + const { + unapprovedMsgCount = 0, + unapprovedPersonalMsgCount = 0, + unapprovedDecryptMsgCount = 0, + unapprovedEncryptionPublicKeyMsgCount = 0, + unapprovedTypedMessagesCount = 0, + } = state.metamask; + + return ( + unapprovedMsgCount + + unapprovedPersonalMsgCount + + unapprovedDecryptMsgCount + + unapprovedEncryptionPublicKeyMsgCount + + unapprovedTypedMessagesCount + ); +} + function getUnapprovedTxCount(state) { const { unapprovedTxs = {} } = state.metamask; return Object.keys(unapprovedTxs).length; diff --git a/ui/store/actions.js b/ui/store/actions.js index db0e1cddb..262b715d6 100644 --- a/ui/store/actions.js +++ b/ui/store/actions.js @@ -13,6 +13,7 @@ import { ENVIRONMENT_TYPE_NOTIFICATION, ORIGIN_METAMASK, POLLING_TOKEN_ENVIRONMENT_TYPES, + MESSAGE_TYPE, } from '../../shared/constants/app'; import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util'; import txHelper from '../helpers/utils/tx-helper'; @@ -1021,6 +1022,96 @@ export function cancelMsg(msgData) { }; } +/** + * Cancels all of the given messages + * + * @param {Array} msgDataList - a list of msg data objects + * @returns {function(*): Promise} + */ +export function cancelMsgs(msgDataList) { + return async (dispatch) => { + dispatch(showLoadingIndication()); + + try { + const msgIds = msgDataList.map((id) => id); + const cancellations = msgDataList.map( + ({ id, type }) => + new Promise((resolve, reject) => { + switch (type) { + case MESSAGE_TYPE.ETH_SIGN_TYPED_DATA: + background.cancelTypedMessage(id, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + return; + case MESSAGE_TYPE.PERSONAL_SIGN: + background.cancelPersonalMessage(id, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + return; + case MESSAGE_TYPE.ETH_DECRYPT: + background.cancelDecryptMessage(id, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + return; + case MESSAGE_TYPE.ETH_GET_ENCRYPTION_PUBLIC_KEY: + background.cancelEncryptionPublicKeyMsg(id, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + return; + case MESSAGE_TYPE.ETH_SIGN: + background.cancelMessage(id, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + return; + default: + reject( + new Error( + `MetaMask Message Signature: Unknown message type: ${id}`, + ), + ); + } + }), + ); + + await Promise.all(cancellations); + const newState = await updateMetamaskStateFromBackground(); + dispatch(updateMetamaskState(newState)); + + msgIds.forEach((id) => { + dispatch(completedTx(id)); + }); + } catch (err) { + log.error(err); + } finally { + if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { + closeNotificationPopup(); + } else { + dispatch(hideLoadingIndication()); + } + } + }; +} + export function cancelPersonalMsg(msgData) { return async (dispatch) => { dispatch(showLoadingIndication()); diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 727835ecf..f956cc841 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -1696,4 +1696,58 @@ describe('Actions', () => { expect(expectedAction.value.id).toStrictEqual(txId); }); }); + + describe('#cancelMsgs', () => { + it('creates COMPLETED_TX with the cancelled messages IDs', async () => { + const store = mockStore(); + + const cancelTypedMessageStub = sinon.stub().callsFake((_, cb) => cb()); + + const cancelPersonalMessageStub = sinon.stub().callsFake((_, cb) => cb()); + + background.getApi.returns({ + cancelTypedMessage: cancelTypedMessageStub, + cancelPersonalMessage: cancelPersonalMessageStub, + getState: sinon.stub().callsFake((cb) => + cb(null, { + currentLocale: 'test', + selectedAddress: '0xFirstAddress', + provider: { + chainId: '0x1', + }, + accounts: { + '0xFirstAddress': { + balance: '0x0', + }, + }, + cachedBalances: { + '0x1': { + '0xFirstAddress': '0x0', + }, + }, + }), + ), + }); + + const msgsList = [ + { id: 7648683973086304, status: 'unapproved', type: 'personal_sign' }, + { + id: 7648683973086303, + status: 'unapproved', + type: 'eth_signTypedData', + }, + ]; + + actions._setBackgroundConnection(background.getApi()); + + await store.dispatch(actions.cancelMsgs(msgsList)); + const resultantActions = store.getActions(); + const expectedActions = resultantActions.filter( + (action) => action.type === 'COMPLETED_TX', + ); + + expect(expectedActions[0].value.id).toStrictEqual(msgsList[0]); + expect(expectedActions[1].value.id).toStrictEqual(msgsList[1]); + }); + }); });