1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 03:12:42 +02:00

Feature/mmi 3009 confirm transaction base code fences (#19335)

* Added code fences

* Continue working on this ticket

* Fixed policies

* Added compliance-row component

* Fixed tests and css

* Fixed invalid locale

* Fixing linting

* Add optional check

* Fixing issues

* Fixed storybook

* Added missing dependency

* ran lavamoat auto

* ran dedupe and lavamoat

* lint

* Removed compliance row

* Removed unneeded package

* Removed unneeded proptyes

* updates mmi packages

* updating lavamoat

* formatting main

* Fixed conflicts

* updates lock file

* Moved code fences to have them all in the same place

* Updated yarn.lock and lavamoat

* remove linebreak

* Improved logic in order to not have many code fences and improve readability

* Fixing proptypes issues with eslint

* runs lavamoat auto

* Testing fixes issue e2e tests

* Testing issues

* Reverting code fences container

* Fixing issue with binding

* Added code fences in proptypes

* Reverting code fences

* Removed institutional from main lavamoat

* Added code fences in confirm transaction base component

* Adding tests for handleMainSubmit

* Improving code

* Added test for handleMainSubmit

* Removed waitFor

---------

Co-authored-by: Antonio Regadas <antonio.regadas@consensys.net>
Co-authored-by: António Regadas <apregadas@gmail.com>
This commit is contained in:
Albert Olivé 2023-06-07 08:43:28 +02:00 committed by GitHub
parent 5f57ad159b
commit 2070e5e42a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 466 additions and 92 deletions

View File

@ -0,0 +1,59 @@
import { TransactionType } from '../constants/transaction';
export default function updateTxData({
txData,
maxFeePerGas,
customTokenAmount,
dappProposedTokenAmount,
currentTokenBalance,
maxPriorityFeePerGas,
baseFeePerGas,
addToAddressBookIfNew,
toAccounts,
toAddress,
name,
}) {
if (txData.type === TransactionType.simpleSend) {
addToAddressBookIfNew(toAddress, toAccounts);
}
if (baseFeePerGas) {
txData.estimatedBaseFee = baseFeePerGas;
}
if (name) {
txData.contractMethodName = name;
}
if (dappProposedTokenAmount) {
txData.dappProposedTokenAmount = dappProposedTokenAmount;
txData.originalApprovalAmount = dappProposedTokenAmount;
}
if (customTokenAmount) {
txData.customTokenAmount = customTokenAmount;
txData.finalApprovalAmount = customTokenAmount;
} else if (dappProposedTokenAmount !== undefined) {
txData.finalApprovalAmount = dappProposedTokenAmount;
}
if (currentTokenBalance) {
txData.currentTokenBalance = currentTokenBalance;
}
if (maxFeePerGas) {
txData.txParams = {
...txData.txParams,
maxFeePerGas,
};
}
if (maxPriorityFeePerGas) {
txData.txParams = {
...txData.txParams,
maxPriorityFeePerGas,
};
}
return txData;
}

View File

@ -0,0 +1,102 @@
import { TransactionType } from '../constants/transaction';
import updateTxData from './updateTxData';
describe('updateTxData', () => {
const mockAddToAddressBookIfNew = jest.fn();
afterEach(() => {
mockAddToAddressBookIfNew.mockClear();
});
it('should add to address book if txData type is simpleSend', () => {
const txData = {
type: TransactionType.simpleSend,
};
updateTxData({
txData,
addToAddressBookIfNew: mockAddToAddressBookIfNew,
toAccounts: 'mockToAccounts',
toAddress: 'mockToAddress',
});
expect(mockAddToAddressBookIfNew).toHaveBeenCalledWith(
'mockToAddress',
'mockToAccounts',
);
});
it('should update estimatedBaseFee if baseFeePerGas is provided', () => {
const txData = {};
const result = updateTxData({
txData,
baseFeePerGas: 'mockBaseFeePerGas',
});
expect(result.estimatedBaseFee).toBe('mockBaseFeePerGas');
});
it('should update contractMethodName if name is provided', () => {
const txData = {};
const result = updateTxData({
txData,
name: 'mockName',
});
expect(result.contractMethodName).toBe('mockName');
});
it('should update dappProposedTokenAmount and originalApprovalAmount if dappProposedTokenAmount is provided', () => {
const txData = {};
const result = updateTxData({
txData,
dappProposedTokenAmount: 'mockDappProposedTokenAmount',
});
expect(result.dappProposedTokenAmount).toBe('mockDappProposedTokenAmount');
expect(result.originalApprovalAmount).toBe('mockDappProposedTokenAmount');
});
it('should update customTokenAmount and finalApprovalAmount if customTokenAmount is provided', () => {
const txData = {};
const result = updateTxData({
txData,
customTokenAmount: 'mockCustomTokenAmount',
});
expect(result.customTokenAmount).toBe('mockCustomTokenAmount');
expect(result.finalApprovalAmount).toBe('mockCustomTokenAmount');
});
it('should update finalApprovalAmount if dappProposedTokenAmount is provided but customTokenAmount is not', () => {
const txData = {};
const result = updateTxData({
txData,
dappProposedTokenAmount: 'mockDappProposedTokenAmount',
});
expect(result.finalApprovalAmount).toBe('mockDappProposedTokenAmount');
});
it('should update currentTokenBalance if currentTokenBalance is provided', () => {
const txData = {};
const result = updateTxData({
txData,
currentTokenBalance: 'mockCurrentTokenBalance',
});
expect(result.currentTokenBalance).toBe('mockCurrentTokenBalance');
});
it('should update maxFeePerGas in txParams if maxFeePerGas is provided', () => {
const txData = { txParams: {} };
const result = updateTxData({
txData,
maxFeePerGas: 'mockMaxFeePerGas',
});
expect(result.txParams.maxFeePerGas).toBe('mockMaxFeePerGas');
});
it('should update maxPriorityFeePerGas in txParams if maxPriorityFeePerGas is provided', () => {
const txData = { txParams: {} };
const result = updateTxData({
txData,
maxPriorityFeePerGas: 'mockMaxPriorityFeePerGas',
});
expect(result.txParams.maxPriorityFeePerGas).toBe(
'mockMaxPriorityFeePerGas',
);
});
});

View File

@ -54,6 +54,7 @@ import { ConfirmData } from '../../components/app/confirm-data';
import { ConfirmTitle } from '../../components/app/confirm-title';
import { ConfirmSubTitle } from '../../components/app/confirm-subtitle';
import { ConfirmGasDisplay } from '../../components/app/confirm-gas-display';
import updateTxData from '../../../shared/modules/updateTxData';
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
@ -133,13 +134,16 @@ export default class ConfirmTransactionBase extends Component {
hardwareWalletRequiresConnection: PropTypes.bool,
isMultiLayerFeeNetwork: PropTypes.bool,
isBuyableChain: PropTypes.bool,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
accountType: PropTypes.string,
isNoteToTraderSupported: PropTypes.bool,
///: END:ONLY_INCLUDE_IN
isApprovalOrRejection: PropTypes.bool,
assetStandard: PropTypes.string,
useCurrencyRateCheck: PropTypes.bool,
isNotification: PropTypes.bool,
accountType: PropTypes.string,
setWaitForConfirmDeepLinkDialog: PropTypes.func,
showTransactionsFailedModal: PropTypes.func,
showCustodianDeepLink: PropTypes.func,
isNoteToTraderSupported: PropTypes.bool,
isMainBetaFlask: PropTypes.bool,
displayAccountBalanceHeader: PropTypes.bool,
};
@ -594,91 +598,49 @@ export default class ConfirmTransactionBase extends Component {
}
handleSubmit() {
const { submitting } = this.state;
if (submitting) {
return;
}
this.props.isMainBetaFlask
? this.handleMainSubmit()
: this.handleMMISubmit();
}
handleMainSubmit() {
const {
sendTransaction,
txData,
history,
mostRecentOverviewPage,
updateCustomNonce,
methodData,
maxFeePerGas,
customTokenAmount,
dappProposedTokenAmount,
currentTokenBalance,
maxPriorityFeePerGas,
baseFeePerGas,
methodData,
addToAddressBookIfNew,
toAccounts,
toAddress,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
accountType,
isNoteToTraderSupported,
///: END:ONLY_INCLUDE_IN
} = this.props;
const {
submitting,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
noteText,
///: END:ONLY_INCLUDE_IN
} = this.state;
const { name } = methodData;
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
if (accountType === 'custody') {
txData.custodyStatus = 'created';
if (isNoteToTraderSupported) {
txData.metadata = {
note: noteText,
};
}
}
///: END:ONLY_INCLUDE_IN
if (txData.type === TransactionType.simpleSend) {
addToAddressBookIfNew(toAddress, toAccounts);
}
if (submitting) {
return;
}
if (baseFeePerGas) {
txData.estimatedBaseFee = baseFeePerGas;
}
if (name) {
txData.contractMethodName = name;
}
if (dappProposedTokenAmount) {
txData.dappProposedTokenAmount = dappProposedTokenAmount;
txData.originalApprovalAmount = dappProposedTokenAmount;
}
if (customTokenAmount) {
txData.customTokenAmount = customTokenAmount;
txData.finalApprovalAmount = customTokenAmount;
} else if (dappProposedTokenAmount !== undefined) {
txData.finalApprovalAmount = dappProposedTokenAmount;
}
if (currentTokenBalance) {
txData.currentTokenBalance = currentTokenBalance;
}
if (maxFeePerGas) {
txData.txParams = {
...txData.txParams,
maxFeePerGas,
};
}
if (maxPriorityFeePerGas) {
txData.txParams = {
...txData.txParams,
maxPriorityFeePerGas,
};
}
updateTxData({
txData,
maxFeePerGas,
customTokenAmount,
dappProposedTokenAmount,
currentTokenBalance,
maxPriorityFeePerGas,
baseFeePerGas,
addToAddressBookIfNew,
toAccounts,
toAddress,
name: methodData.name,
});
this.setState(
{
@ -708,11 +670,128 @@ export default class ConfirmTransactionBase extends Component {
if (!this._isMounted) {
return;
}
this.setState({
submitting: false,
submitError: error.message,
});
updateCustomNonce('');
});
},
);
}
handleMMISubmit() {
const {
sendTransaction,
txData,
history,
mostRecentOverviewPage,
updateCustomNonce,
unapprovedTxCount,
accountType,
isNotification,
setWaitForConfirmDeepLinkDialog,
showTransactionsFailedModal,
fromAddress,
isNoteToTraderSupported,
methodData,
maxFeePerGas,
customTokenAmount,
dappProposedTokenAmount,
currentTokenBalance,
maxPriorityFeePerGas,
baseFeePerGas,
addToAddressBookIfNew,
toAccounts,
toAddress,
} = this.props;
const { noteText } = this.state;
if (accountType === 'custody') {
txData.custodyStatus = 'created';
if (isNoteToTraderSupported) {
txData.metadata = {
note: noteText,
};
}
}
updateTxData({
txData,
maxFeePerGas,
customTokenAmount,
dappProposedTokenAmount,
currentTokenBalance,
maxPriorityFeePerGas,
baseFeePerGas,
addToAddressBookIfNew,
toAccounts,
toAddress,
name: methodData.name,
});
this.setState(
{
submitting: true,
submitError: null,
},
() => {
this._removeBeforeUnload();
if (txData.custodyStatus) {
setWaitForConfirmDeepLinkDialog(true);
}
sendTransaction(txData)
.then(() => {
if (!this._isMounted) {
return;
}
if (txData.custodyStatus) {
this.props.showCustodianDeepLink({
fromAddress,
closeNotification: isNotification && unapprovedTxCount === 1,
txId: txData.id,
onDeepLinkFetched: () => {
this.context.trackEvent({
category: 'MMI',
event: 'Show deeplink for transaction',
});
},
onDeepLinkShown: () => {
this.props.clearConfirmTransaction();
this.setState({ submitting: false }, () => {
history.push(mostRecentOverviewPage);
updateCustomNonce('');
});
},
});
} else {
this.setState(
{
submitting: false,
},
() => {
history.push(mostRecentOverviewPage);
updateCustomNonce('');
},
);
}
})
.catch((error) => {
if (!this._isMounted) {
return;
}
showTransactionsFailedModal(error.message, isNotification);
this.setState({
submitting: false,
submitError: error.message,
});
setWaitForConfirmDeepLinkDialog(true);
updateCustomNonce('');
});
},
@ -855,7 +934,6 @@ export default class ConfirmTransactionBase extends Component {
userAcknowledgedGasMissing,
showWarningModal,
} = this.state;
const { name } = methodData;
const { valid, errorKey } = this.getErrorKey();
const hasSimulationError = Boolean(txData.simulationFails);

View File

@ -1,7 +1,10 @@
import { connect } from 'react-redux';
import { compose } from 'redux';
import { withRouter } from 'react-router-dom';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
import { showCustodianDeepLink } from '@metamask-institutional/extension';
import { mmiActionsFactory } from '../../store/institutional/institution-background';
///: END:ONLY_INCLUDE_IN
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
import {
@ -48,7 +51,12 @@ import {
getSendToAccounts,
getProviderConfig,
} from '../../ducks/metamask/metamask';
import { addHexPrefix } from '../../../app/scripts/lib/util';
import {
addHexPrefix,
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
getEnvironmentType,
///: END:ONLY_INCLUDE_IN
} from '../../../app/scripts/lib/util';
import {
parseStandardTokenTransactionData,
@ -63,8 +71,11 @@ import {
import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app';
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
import { CUSTOM_GAS_ESTIMATE } from '../../../shared/constants/gas';
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
import { getAccountType } from '../../selectors/selectors';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app';
import { getIsNoteToTraderSupported } from '../../selectors/institutional/selectors';
///: END:ONLY_INCLUDE_IN
import {
TransactionStatus,
@ -100,6 +111,11 @@ const mapStateToProps = (state, ownProps) => {
const { id: paramsTransactionId } = params;
const isMainnet = getIsMainnet(state);
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const envType = getEnvironmentType();
const isNotification = envType === ENVIRONMENT_TYPE_NOTIFICATION;
///: END:ONLY_INCLUDE_IN
const isGasEstimatesLoading = getIsGasEstimatesLoading(state);
const gasLoadingAnimationIsShowing = getGasLoadingAnimationIsShowing(state);
const isBuyableChain = getIsBuyableChain(state);
@ -199,29 +215,20 @@ const mapStateToProps = (state, ownProps) => {
txParamsAreDappSuggested(fullTxData);
const fromAddressIsLedger = isAddressLedger(state, fromAddress);
const nativeCurrency = getNativeCurrency(state);
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const accountType = getAccountType(state, fromAddress);
const fromChecksumHexAddress = toChecksumHexAddress(fromAddress);
const isNoteToTraderSupported = getIsNoteToTraderSupported(
state,
fromChecksumHexAddress,
);
///: END:ONLY_INCLUDE_IN
const hardwareWalletRequiresConnection =
doesAddressRequireLedgerHidConnection(state, fromAddress);
const isMultiLayerFeeNetwork = getIsMultiLayerFeeNetwork(state);
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const accountType = getAccountType(state);
const fromChecksumHexAddress = toChecksumHexAddress(fromAddress);
let isNoteToTraderSupported = false;
if (
state.metamask.custodyAccountDetails &&
state.metamask.custodyAccountDetails[fromChecksumHexAddress]
) {
const { custodianName } =
state.metamask.custodyAccountDetails[fromChecksumHexAddress];
isNoteToTraderSupported = state.metamask.mmiConfiguration?.custodians?.find(
(custodian) => custodian.name === custodianName,
)?.isNoteToTraderSupported;
}
///: END:ONLY_INCLUDE_IN
return {
balance,
fromAddress,
@ -275,11 +282,15 @@ const mapStateToProps = (state, ownProps) => {
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
accountType,
isNoteToTraderSupported,
isNotification,
///: END:ONLY_INCLUDE_IN
};
};
export const mapDispatchToProps = (dispatch) => {
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
const mmiActions = mmiActionsFactory();
///: END:ONLY_INCLUDE_IN
return {
tryReverseResolveAddress: (address) => {
return dispatch(tryReverseResolveAddress(address));
@ -316,6 +327,45 @@ export const mapDispatchToProps = (dispatch) => {
dispatch(addToAddressBook(hexPrefixedAddress, nickname));
}
},
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
getCustodianConfirmDeepLink: (id) =>
dispatch(mmiActions.getCustodianConfirmDeepLink(id)),
showCustodyConfirmLink: ({ link, address, closeNotification, custodyId }) =>
dispatch(
mmiActions.showCustodyConfirmLink({
link,
address,
closeNotification,
custodyId,
}),
),
showTransactionsFailedModal: (errorMessage, closeNotification) =>
dispatch(
showModal({
name: 'TRANSACTION_FAILED',
errorMessage,
closeNotification,
}),
),
showCustodianDeepLink: ({
txId,
fromAddress,
closeNotification,
onDeepLinkFetched,
onDeepLinkShown,
}) =>
showCustodianDeepLink({
dispatch,
mmiActions,
txId,
fromAddress,
closeNotification,
onDeepLinkFetched,
onDeepLinkShown,
}),
setWaitForConfirmDeepLinkDialog: (wait) =>
dispatch(mmiActions.setWaitForConfirmDeepLinkDialog(wait)),
///: END:ONLY_INCLUDE_IN
};
};
@ -328,6 +378,12 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
...otherDispatchProps
} = dispatchProps;
let isMainBetaFlask = false;
///: BEGIN:ONLY_INCLUDE_IN(build-main,build-beta,build-flask)
isMainBetaFlask = true;
///: END:ONLY_INCLUDE_IN
return {
...stateProps,
...otherDispatchProps,
@ -341,6 +397,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
transaction: txData,
});
},
isMainBetaFlask,
};
};

View File

@ -1,6 +1,7 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { fireEvent } from '@testing-library/react';
import { renderWithProvider } from '../../../test/lib/render-helpers';
import { setBackgroundConnection } from '../../../test/jest';
@ -235,6 +236,70 @@ describe('Confirm Transaction Base', () => {
expect(getByTestId('transaction-note')).toBeInTheDocument();
});
it('handleMainSubmit calls sendTransaction with correct arguments', async () => {
const newMockedStore = {
...mockedStore,
appState: {
...mockedStore.appState,
gasLoadingAnimationIsShowing: false,
},
metamask: {
...mockedStore.metamask,
accounts: {
[mockTxParamsFromAddress]: {
balance: '0x1000000000000000000',
address: mockTxParamsFromAddress,
},
},
gasEstimateType: GasEstimateTypes.feeMarket,
networkDetails: {
...mockedStore.metamask.networkDetails,
EIPS: {
1559: true,
},
},
customGas: {
gasLimit: '0x5208',
gasPrice: '0x59682f00',
},
noGasPrice: false,
},
send: {
...mockedStore.send,
gas: {
...mockedStore.send.gas,
gasEstimateType: GasEstimateTypes.legacy,
gasFeeEstimates: {
low: '0',
medium: '1',
high: '2',
},
},
hasSimulationError: false,
userAcknowledgedGasMissing: false,
submitting: false,
hardwareWalletRequiresConnection: false,
gasIsLoading: false,
gasFeeIsCustom: true,
},
};
const store = configureMockStore(middleware)(newMockedStore);
const sendTransaction = jest.fn().mockResolvedValue();
const { getByTestId } = renderWithProvider(
<ConfirmTransactionBase
actionKey="confirm"
sendTransaction={sendTransaction}
toAddress={mockPropsToAddress}
toAccounts={[{ address: mockPropsToAddress }]}
/>,
store,
);
const confirmButton = getByTestId('page-container-footer-next');
fireEvent.click(confirmButton);
expect(sendTransaction).toHaveBeenCalled();
});
describe('when rendering the recipient value', () => {
describe(`when the transaction is a ${TransactionType.simpleSend} type`, () => {
it(`should use txParams.to address`, () => {

View File

@ -78,3 +78,16 @@ export function getMMIConfiguration(state) {
export function getInteractiveReplacementToken(state) {
return state.metamask.interactiveReplacementToken || {};
}
export function getIsNoteToTraderSupported(state, fromChecksumHexAddress) {
let isNoteToTraderSupported = false;
if (state.metamask.custodyAccountDetails?.[fromChecksumHexAddress]) {
const { custodianName } =
state.metamask.custodyAccountDetails[fromChecksumHexAddress];
isNoteToTraderSupported = state.metamask.mmiConfiguration?.custodians?.find(
(custodian) => custodian.name === custodianName,
)?.isNoteToTraderSupported;
}
return isNoteToTraderSupported;
}