1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 09:57:02 +01:00

"Cancel/reject all" for signature requests #13201 (#13786)

This commit is contained in:
dragana8 2022-05-16 20:36:19 +02:00 committed by GitHub
parent c2cd6f8097
commit a0c4febfce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 251 additions and 3 deletions

View File

@ -20,6 +20,10 @@
@media screen and (min-width: $break-large) { @media screen and (min-width: $break-large) {
height: 620px; height: 620px;
} }
&__reject {
padding-bottom: 20px;
}
} }
&__typed-container { &__typed-container {

View File

@ -38,6 +38,9 @@ export default class SignatureRequestOriginal extends Component {
hardwareWalletRequiresConnection: PropTypes.bool, hardwareWalletRequiresConnection: PropTypes.bool,
isLedgerWallet: PropTypes.bool, isLedgerWallet: PropTypes.bool,
nativeCurrency: PropTypes.string.isRequired, nativeCurrency: PropTypes.string.isRequired,
messagesCount: PropTypes.number,
showRejectTransactionsConfirmationModal: PropTypes.func.isRequired,
cancelAll: PropTypes.func.isRequired,
}; };
state = { 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 = () => { render = () => {
const { messagesCount } = this.props;
const { t } = this.context;
const rejectNText = t('rejectTxsN', [messagesCount]);
return ( return (
<div className="request-signature__container"> <div className="request-signature__container">
{this.renderHeader()} {this.renderHeader()}
@ -326,6 +353,15 @@ export default class SignatureRequestOriginal extends Component {
</div> </div>
) : null} ) : null}
{this.renderFooter()} {this.renderFooter()}
{messagesCount > 1 ? (
<Button
type="link"
className="request-signature__container__reject"
onClick={() => this.handleCancelAll()}
>
{rejectNText}
</Button>
) : null}
</div> </div>
); );
}; };

View File

@ -3,14 +3,16 @@ import { compose } from 'redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { MESSAGE_TYPE } from '../../../../shared/constants/app'; import { MESSAGE_TYPE } from '../../../../shared/constants/app';
import { goHome } from '../../../store/actions'; import { goHome, cancelMsgs, showModal } from '../../../store/actions';
import { import {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
conversionRateSelector, conversionRateSelector,
getSubjectMetadata, getSubjectMetadata,
doesAddressRequireLedgerHidConnection, doesAddressRequireLedgerHidConnection,
unconfirmedMessagesHashSelector,
getTotalUnapprovedMessagesCount,
} from '../../../selectors'; } 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 { clearConfirmTransaction } from '../../../ducks/confirm-transaction/confirm-transaction.duck';
import { getMostRecentOverviewPage } from '../../../ducks/history/history'; import { getMostRecentOverviewPage } from '../../../ducks/history/history';
import { import {
@ -29,6 +31,8 @@ function mapStateToProps(state, ownProps) {
from, from,
); );
const isLedgerWallet = isAddressLedger(state, from); const isLedgerWallet = isAddressLedger(state, from);
const messagesList = unconfirmedMessagesHashSelector(state);
const messagesCount = getTotalUnapprovedMessagesCount(state);
return { return {
requester: null, requester: null,
@ -41,6 +45,8 @@ function mapStateToProps(state, ownProps) {
// not passed to component // not passed to component
allAccounts: accountsWithSendEtherInfoSelector(state), allAccounts: accountsWithSendEtherInfoSelector(state),
subjectMetadata: getSubjectMetadata(state), subjectMetadata: getSubjectMetadata(state),
messagesList,
messagesCount,
}; };
} }
@ -48,6 +54,19 @@ function mapDispatchToProps(dispatch) {
return { return {
goHome: () => dispatch(goHome()), goHome: () => dispatch(goHome()),
clearConfirmTransaction: () => dispatch(clearConfirmTransaction()), 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, txData,
} = ownProps; } = ownProps;
const { allAccounts, ...otherStateProps } = stateProps; const { allAccounts, messagesList, ...otherStateProps } = stateProps;
const { const {
type, type,
@ -71,6 +90,8 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
const fromAccount = getAccountByAddress(allAccounts, from); const fromAccount = getAccountByAddress(allAccounts, from);
const { cancelAll: dispatchCancelAll } = dispatchProps;
let cancel; let cancel;
let sign; let sign;
if (type === MESSAGE_TYPE.PERSONAL_SIGN) { if (type === MESSAGE_TYPE.PERSONAL_SIGN) {
@ -92,6 +113,7 @@ function mergeProps(stateProps, dispatchProps, ownProps) {
txData, txData,
cancel, cancel,
sign, sign,
cancelAll: () => dispatchCancelAll(valuesFor(messagesList)),
}; };
} }

View File

@ -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 unapprovedMsgCountSelector = (state) => state.metamask.unapprovedMsgCount;
const unapprovedPersonalMsgCountSelector = (state) => const unapprovedPersonalMsgCountSelector = (state) =>
state.metamask.unapprovedPersonalMsgCount; state.metamask.unapprovedPersonalMsgCount;

View File

@ -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) { function getUnapprovedTxCount(state) {
const { unapprovedTxs = {} } = state.metamask; const { unapprovedTxs = {} } = state.metamask;
return Object.keys(unapprovedTxs).length; return Object.keys(unapprovedTxs).length;

View File

@ -13,6 +13,7 @@ import {
ENVIRONMENT_TYPE_NOTIFICATION, ENVIRONMENT_TYPE_NOTIFICATION,
ORIGIN_METAMASK, ORIGIN_METAMASK,
POLLING_TOKEN_ENVIRONMENT_TYPES, POLLING_TOKEN_ENVIRONMENT_TYPES,
MESSAGE_TYPE,
} from '../../shared/constants/app'; } from '../../shared/constants/app';
import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util'; import { hasUnconfirmedTransactions } from '../helpers/utils/confirm-tx.util';
import txHelper from '../helpers/utils/tx-helper'; import txHelper from '../helpers/utils/tx-helper';
@ -1021,6 +1022,96 @@ export function cancelMsg(msgData) {
}; };
} }
/**
* Cancels all of the given messages
*
* @param {Array<object>} msgDataList - a list of msg data objects
* @returns {function(*): Promise<void>}
*/
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) { export function cancelPersonalMsg(msgData) {
return async (dispatch) => { return async (dispatch) => {
dispatch(showLoadingIndication()); dispatch(showLoadingIndication());

View File

@ -1696,4 +1696,58 @@ describe('Actions', () => {
expect(expectedAction.value.id).toStrictEqual(txId); 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]);
});
});
}); });