mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +01:00
60f149178c
* fix(custodial-signing): set approved status in tx controller in MMI logic * Fixed the issue that was not showing custody-confirm-link --------- Co-authored-by: Shane Terence Odlum <shane.odlum@consensys.net>
1046 lines
30 KiB
JavaScript
1046 lines
30 KiB
JavaScript
import React, { Component } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import ConfirmPageContainer from '../../components/app/confirm-page-container';
|
|
import { isBalanceSufficient } from '../send/send.utils';
|
|
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
|
import {
|
|
INSUFFICIENT_FUNDS_ERROR_KEY,
|
|
GAS_LIMIT_TOO_LOW_ERROR_KEY,
|
|
ETH_GAS_PRICE_FETCH_WARNING_KEY,
|
|
GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
|
|
} from '../../helpers/constants/error-keys';
|
|
import UserPreferencedCurrencyDisplay from '../../components/app/user-preferenced-currency-display';
|
|
|
|
import { PRIMARY, SECONDARY } from '../../helpers/constants/common';
|
|
import TextField from '../../components/ui/text-field';
|
|
import SimulationErrorMessage from '../../components/ui/simulation-error-message';
|
|
import { MetaMetricsEventCategory } from '../../../shared/constants/metametrics';
|
|
import {
|
|
TransactionType,
|
|
TransactionStatus,
|
|
} from '../../../shared/constants/transaction';
|
|
import { getMethodName } from '../../helpers/utils/metrics';
|
|
import {
|
|
getTransactionTypeTitle,
|
|
isLegacyTransaction,
|
|
} from '../../helpers/utils/transactions.util';
|
|
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
import NoteToTrader from '../../components/institutional/note-to-trader';
|
|
///: END:ONLY_INCLUDE_IN
|
|
|
|
import { TransactionModalContextProvider } from '../../contexts/transaction-modal';
|
|
import TransactionDetail from '../../components/app/transaction-detail/transaction-detail.component';
|
|
import TransactionDetailItem from '../../components/app/transaction-detail-item/transaction-detail-item.component';
|
|
import LoadingHeartBeat from '../../components/ui/loading-heartbeat';
|
|
import LedgerInstructionField from '../../components/app/ledger-instruction-field';
|
|
import {
|
|
disconnectGasFeeEstimatePoller,
|
|
getGasFeeEstimatesAndStartPolling,
|
|
addPollingTokenToAppState,
|
|
removePollingTokenFromAppState,
|
|
} from '../../store/actions';
|
|
|
|
import { MIN_GAS_LIMIT_DEC } from '../send/send.constants';
|
|
|
|
import { NETWORK_TO_NAME_MAP } from '../../../shared/constants/network';
|
|
import {
|
|
addHexes,
|
|
hexToDecimal,
|
|
} from '../../../shared/modules/conversion.utils';
|
|
import TransactionAlerts from '../../components/app/transaction-alerts';
|
|
import { ConfirmHexData } from '../../components/app/confirm-hexdata';
|
|
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 = {
|
|
t: PropTypes.func,
|
|
trackEvent: PropTypes.func,
|
|
};
|
|
|
|
static propTypes = {
|
|
// react-router props
|
|
history: PropTypes.object,
|
|
// Redux props
|
|
balance: PropTypes.string,
|
|
cancelTransaction: PropTypes.func,
|
|
cancelAllTransactions: PropTypes.func,
|
|
clearConfirmTransaction: PropTypes.func,
|
|
conversionRate: PropTypes.number,
|
|
fromAddress: PropTypes.string,
|
|
fromName: PropTypes.string,
|
|
hexTransactionAmount: PropTypes.string,
|
|
hexMaximumTransactionFee: PropTypes.string,
|
|
hexTransactionTotal: PropTypes.string,
|
|
methodData: PropTypes.object,
|
|
nonce: PropTypes.string,
|
|
useNonceField: PropTypes.bool,
|
|
customNonceValue: PropTypes.string,
|
|
updateCustomNonce: PropTypes.func,
|
|
sendTransaction: PropTypes.func,
|
|
showTransactionConfirmedModal: PropTypes.func,
|
|
showRejectTransactionsConfirmationModal: PropTypes.func,
|
|
toAccounts: PropTypes.array,
|
|
toAddress: PropTypes.string,
|
|
tokenData: PropTypes.object,
|
|
tokenProps: PropTypes.object,
|
|
toName: PropTypes.string,
|
|
toEns: PropTypes.string,
|
|
toNickname: PropTypes.string,
|
|
transactionStatus: PropTypes.string,
|
|
txData: PropTypes.object,
|
|
unapprovedTxCount: PropTypes.number,
|
|
customGas: PropTypes.object,
|
|
addToAddressBookIfNew: PropTypes.func,
|
|
// Component props
|
|
actionKey: PropTypes.string,
|
|
contentComponent: PropTypes.node,
|
|
dataComponent: PropTypes.node,
|
|
dataHexComponent: PropTypes.node,
|
|
tokenAddress: PropTypes.string,
|
|
customTokenAmount: PropTypes.string,
|
|
dappProposedTokenAmount: PropTypes.string,
|
|
currentTokenBalance: PropTypes.string,
|
|
onEdit: PropTypes.func,
|
|
subtitleComponent: PropTypes.node,
|
|
title: PropTypes.string,
|
|
image: PropTypes.string,
|
|
type: PropTypes.string,
|
|
getNextNonce: PropTypes.func,
|
|
nextNonce: PropTypes.number,
|
|
tryReverseResolveAddress: PropTypes.func.isRequired,
|
|
hideSenderToRecipient: PropTypes.bool,
|
|
showAccountInHeader: PropTypes.bool,
|
|
mostRecentOverviewPage: PropTypes.string.isRequired,
|
|
isEthGasPrice: PropTypes.bool,
|
|
noGasPrice: PropTypes.bool,
|
|
setDefaultHomeActiveTabName: PropTypes.func,
|
|
primaryTotalTextOverride: PropTypes.string,
|
|
secondaryTotalTextOverride: PropTypes.string,
|
|
gasIsLoading: PropTypes.bool,
|
|
primaryTotalTextOverrideMaxAmount: PropTypes.string,
|
|
useNativeCurrencyAsPrimaryCurrency: PropTypes.bool,
|
|
maxFeePerGas: PropTypes.string,
|
|
maxPriorityFeePerGas: PropTypes.string,
|
|
baseFeePerGas: PropTypes.string,
|
|
gasFeeIsCustom: PropTypes.bool,
|
|
showLedgerSteps: PropTypes.bool.isRequired,
|
|
nativeCurrency: PropTypes.string,
|
|
supportsEIP1559: PropTypes.bool,
|
|
hardwareWalletRequiresConnection: PropTypes.bool,
|
|
isMultiLayerFeeNetwork: PropTypes.bool,
|
|
isBuyableChain: PropTypes.bool,
|
|
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,
|
|
};
|
|
|
|
state = {
|
|
submitting: false,
|
|
submitError: null,
|
|
submitWarning: '',
|
|
ethGasPriceWarning: '',
|
|
editingGas: false,
|
|
userAcknowledgedGasMissing: false,
|
|
showWarningModal: false,
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
noteText: '',
|
|
///: END:ONLY_INCLUDE_IN
|
|
};
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const {
|
|
transactionStatus,
|
|
showTransactionConfirmedModal,
|
|
history,
|
|
clearConfirmTransaction,
|
|
nextNonce,
|
|
customNonceValue,
|
|
toAddress,
|
|
tryReverseResolveAddress,
|
|
isEthGasPrice,
|
|
setDefaultHomeActiveTabName,
|
|
} = this.props;
|
|
const {
|
|
customNonceValue: prevCustomNonceValue,
|
|
nextNonce: prevNextNonce,
|
|
toAddress: prevToAddress,
|
|
transactionStatus: prevTxStatus,
|
|
isEthGasPrice: prevIsEthGasPrice,
|
|
} = prevProps;
|
|
const statusUpdated = transactionStatus !== prevTxStatus;
|
|
const txDroppedOrConfirmed =
|
|
transactionStatus === TransactionStatus.dropped ||
|
|
transactionStatus === TransactionStatus.confirmed;
|
|
|
|
if (
|
|
nextNonce !== prevNextNonce ||
|
|
customNonceValue !== prevCustomNonceValue
|
|
) {
|
|
if (nextNonce !== null && customNonceValue > nextNonce) {
|
|
this.setState({
|
|
submitWarning: this.context.t('nextNonceWarning', [nextNonce]),
|
|
});
|
|
} else {
|
|
this.setState({ submitWarning: '' });
|
|
}
|
|
}
|
|
|
|
if (statusUpdated && txDroppedOrConfirmed) {
|
|
showTransactionConfirmedModal({
|
|
onSubmit: () => {
|
|
clearConfirmTransaction();
|
|
setDefaultHomeActiveTabName('activity').then(() => {
|
|
history.push(DEFAULT_ROUTE);
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
if (toAddress && toAddress !== prevToAddress) {
|
|
tryReverseResolveAddress(toAddress);
|
|
}
|
|
|
|
if (isEthGasPrice !== prevIsEthGasPrice) {
|
|
if (isEthGasPrice) {
|
|
this.setState({
|
|
ethGasPriceWarning: this.context.t(ETH_GAS_PRICE_FETCH_WARNING_KEY),
|
|
});
|
|
} else {
|
|
this.setState({
|
|
ethGasPriceWarning: '',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
getErrorKey() {
|
|
const {
|
|
balance,
|
|
conversionRate,
|
|
hexMaximumTransactionFee,
|
|
txData: { txParams: { value: amount } = {} } = {},
|
|
customGas,
|
|
noGasPrice,
|
|
gasFeeIsCustom,
|
|
} = this.props;
|
|
|
|
const insufficientBalance =
|
|
balance &&
|
|
!isBalanceSufficient({
|
|
amount,
|
|
gasTotal: hexMaximumTransactionFee || '0x0',
|
|
balance,
|
|
conversionRate,
|
|
});
|
|
|
|
if (insufficientBalance) {
|
|
return {
|
|
valid: false,
|
|
errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
|
|
};
|
|
}
|
|
|
|
if (hexToDecimal(customGas.gasLimit) < Number(MIN_GAS_LIMIT_DEC)) {
|
|
return {
|
|
valid: false,
|
|
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
|
|
};
|
|
}
|
|
|
|
if (noGasPrice && !gasFeeIsCustom) {
|
|
return {
|
|
valid: false,
|
|
errorKey: GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
|
|
};
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
};
|
|
}
|
|
|
|
handleEditGas() {
|
|
const {
|
|
actionKey,
|
|
txData: { origin },
|
|
methodData = {},
|
|
} = this.props;
|
|
|
|
this.context.trackEvent({
|
|
category: MetaMetricsEventCategory.Transactions,
|
|
event: 'User clicks "Edit" on gas',
|
|
properties: {
|
|
action: 'Confirm Screen',
|
|
legacy_event: true,
|
|
recipientKnown: null,
|
|
functionType:
|
|
actionKey ||
|
|
getMethodName(methodData.name) ||
|
|
TransactionType.contractInteraction,
|
|
origin,
|
|
},
|
|
});
|
|
|
|
this.setState({ editingGas: true });
|
|
}
|
|
|
|
handleCloseEditGas() {
|
|
this.setState({ editingGas: false });
|
|
}
|
|
|
|
setUserAcknowledgedGasMissing() {
|
|
this.setState({ userAcknowledgedGasMissing: true });
|
|
}
|
|
|
|
renderDetails() {
|
|
const {
|
|
primaryTotalTextOverride,
|
|
secondaryTotalTextOverride,
|
|
hexMaximumTransactionFee,
|
|
hexTransactionTotal,
|
|
useNonceField,
|
|
customNonceValue,
|
|
updateCustomNonce,
|
|
nextNonce,
|
|
getNextNonce,
|
|
txData,
|
|
useNativeCurrencyAsPrimaryCurrency,
|
|
primaryTotalTextOverrideMaxAmount,
|
|
showLedgerSteps,
|
|
isMultiLayerFeeNetwork,
|
|
nativeCurrency,
|
|
isBuyableChain,
|
|
useCurrencyRateCheck,
|
|
} = this.props;
|
|
const { t } = this.context;
|
|
const { userAcknowledgedGasMissing } = this.state;
|
|
|
|
const { valid } = this.getErrorKey();
|
|
const isDisabled = () => {
|
|
return userAcknowledgedGasMissing ? false : !valid;
|
|
};
|
|
|
|
const hasSimulationError = Boolean(txData.simulationFails);
|
|
|
|
const renderSimulationFailureWarning =
|
|
hasSimulationError && !userAcknowledgedGasMissing;
|
|
const networkName = NETWORK_TO_NAME_MAP[txData.chainId];
|
|
|
|
const renderTotalMaxAmount = () => {
|
|
if (
|
|
primaryTotalTextOverrideMaxAmount === undefined &&
|
|
secondaryTotalTextOverride === undefined
|
|
) {
|
|
// Native Send
|
|
return (
|
|
<UserPreferencedCurrencyDisplay
|
|
type={PRIMARY}
|
|
key="total-max-amount"
|
|
value={addHexes(txData.txParams.value, hexMaximumTransactionFee)}
|
|
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Token send
|
|
return useNativeCurrencyAsPrimaryCurrency
|
|
? primaryTotalTextOverrideMaxAmount
|
|
: secondaryTotalTextOverride;
|
|
};
|
|
|
|
const renderTotalDetailTotal = () => {
|
|
if (
|
|
primaryTotalTextOverride === undefined &&
|
|
secondaryTotalTextOverride === undefined
|
|
) {
|
|
return (
|
|
<div className="confirm-page-container-content__total-value">
|
|
<LoadingHeartBeat estimateUsed={this.props.txData?.userFeeLevel} />
|
|
<UserPreferencedCurrencyDisplay
|
|
type={PRIMARY}
|
|
key="total-detail-value"
|
|
value={hexTransactionTotal}
|
|
hideLabel={!useNativeCurrencyAsPrimaryCurrency}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
return useNativeCurrencyAsPrimaryCurrency
|
|
? primaryTotalTextOverride
|
|
: secondaryTotalTextOverride;
|
|
};
|
|
|
|
const renderTotalDetailText = () => {
|
|
if (
|
|
primaryTotalTextOverride === undefined &&
|
|
secondaryTotalTextOverride === undefined
|
|
) {
|
|
return (
|
|
<div className="confirm-page-container-content__total-value">
|
|
<LoadingHeartBeat estimateUsed={this.props.txData?.userFeeLevel} />
|
|
<UserPreferencedCurrencyDisplay
|
|
type={SECONDARY}
|
|
key="total-detail-text"
|
|
value={hexTransactionTotal}
|
|
hideLabel={Boolean(useNativeCurrencyAsPrimaryCurrency)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
return useNativeCurrencyAsPrimaryCurrency
|
|
? secondaryTotalTextOverride
|
|
: primaryTotalTextOverride;
|
|
};
|
|
|
|
const nonceField = useNonceField ? (
|
|
<div>
|
|
<div className="confirm-detail-row">
|
|
<div className="confirm-detail-row__label">
|
|
{t('nonceFieldHeading')}
|
|
</div>
|
|
<div className="custom-nonce-input">
|
|
<TextField
|
|
type="number"
|
|
min={0}
|
|
placeholder={
|
|
typeof nextNonce === 'number' ? nextNonce.toString() : null
|
|
}
|
|
onChange={({ target: { value } }) => {
|
|
if (!value.length || Number(value) < 0) {
|
|
updateCustomNonce('');
|
|
} else {
|
|
updateCustomNonce(String(Math.floor(value)));
|
|
}
|
|
getNextNonce();
|
|
}}
|
|
fullWidth
|
|
margin="dense"
|
|
value={customNonceValue || ''}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null;
|
|
|
|
const simulationFailureWarning = () => (
|
|
<div
|
|
className="confirm-page-container-content__error-container"
|
|
key="confirm-transaction-base_simulation-error-container"
|
|
>
|
|
<SimulationErrorMessage
|
|
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
|
|
setUserAcknowledgedGasMissing={() =>
|
|
this.setUserAcknowledgedGasMissing()
|
|
}
|
|
/>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="confirm-page-container-content__details">
|
|
<TransactionAlerts
|
|
setUserAcknowledgedGasMissing={() =>
|
|
this.setUserAcknowledgedGasMissing()
|
|
}
|
|
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
|
|
nativeCurrency={nativeCurrency}
|
|
networkName={networkName}
|
|
type={txData.type}
|
|
isBuyableChain={isBuyableChain}
|
|
/>
|
|
<TransactionDetail
|
|
disabled={isDisabled()}
|
|
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
|
|
onEdit={
|
|
renderSimulationFailureWarning ? null : () => this.handleEditGas()
|
|
}
|
|
rows={[
|
|
renderSimulationFailureWarning && simulationFailureWarning(),
|
|
!renderSimulationFailureWarning && (
|
|
<ConfirmGasDisplay
|
|
key="confirm-transaction-base_confirm-gas-display"
|
|
userAcknowledgedGasMissing={userAcknowledgedGasMissing}
|
|
/>
|
|
),
|
|
!isMultiLayerFeeNetwork && (
|
|
<TransactionDetailItem
|
|
key="confirm-transaction-base-total-item"
|
|
detailTitle={t('total')}
|
|
detailText={useCurrencyRateCheck && renderTotalDetailText()}
|
|
detailTotal={renderTotalDetailTotal()}
|
|
subTitle={t('transactionDetailGasTotalSubtitle')}
|
|
subText={
|
|
<div className="confirm-page-container-content__total-amount">
|
|
<LoadingHeartBeat
|
|
estimateUsed={this.props.txData?.userFeeLevel}
|
|
/>
|
|
<strong key="editGasSubTextAmountLabel">
|
|
{t('editGasSubTextAmountLabel')}
|
|
</strong>{' '}
|
|
{renderTotalMaxAmount()}
|
|
</div>
|
|
}
|
|
/>
|
|
),
|
|
]}
|
|
/>
|
|
{nonceField}
|
|
{showLedgerSteps ? (
|
|
<LedgerInstructionField
|
|
showDataInstruction={Boolean(txData.txParams?.data)}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderData() {
|
|
const { txData, dataComponent } = this.props;
|
|
const {
|
|
txParams: { data },
|
|
} = txData;
|
|
if (!data) {
|
|
return null;
|
|
}
|
|
return <ConfirmData txData={txData} dataComponent={dataComponent} />;
|
|
}
|
|
|
|
renderDataHex() {
|
|
const { txData, dataHexComponent } = this.props;
|
|
const {
|
|
txParams: { data, to },
|
|
} = txData;
|
|
if (!data || !to) {
|
|
return null;
|
|
}
|
|
return (
|
|
<ConfirmHexData txData={txData} dataHexComponent={dataHexComponent} />
|
|
);
|
|
}
|
|
|
|
handleEdit() {
|
|
const {
|
|
txData,
|
|
tokenData,
|
|
tokenProps,
|
|
onEdit,
|
|
actionKey,
|
|
txData: { origin },
|
|
methodData = {},
|
|
} = this.props;
|
|
|
|
this.context.trackEvent({
|
|
category: MetaMetricsEventCategory.Transactions,
|
|
event: 'Edit Transaction',
|
|
properties: {
|
|
action: 'Confirm Screen',
|
|
legacy_event: true,
|
|
recipientKnown: null,
|
|
functionType:
|
|
actionKey ||
|
|
getMethodName(methodData.name) ||
|
|
TransactionType.contractInteraction,
|
|
origin,
|
|
},
|
|
});
|
|
|
|
onEdit({ txData, tokenData, tokenProps });
|
|
}
|
|
|
|
handleCancelAll() {
|
|
const {
|
|
cancelAllTransactions,
|
|
clearConfirmTransaction,
|
|
history,
|
|
mostRecentOverviewPage,
|
|
showRejectTransactionsConfirmationModal,
|
|
unapprovedTxCount,
|
|
} = this.props;
|
|
|
|
showRejectTransactionsConfirmationModal({
|
|
unapprovedTxCount,
|
|
onSubmit: async () => {
|
|
this._removeBeforeUnload();
|
|
await cancelAllTransactions();
|
|
clearConfirmTransaction();
|
|
history.push(mostRecentOverviewPage);
|
|
},
|
|
});
|
|
}
|
|
|
|
async handleCancel() {
|
|
const {
|
|
txData,
|
|
cancelTransaction,
|
|
history,
|
|
mostRecentOverviewPage,
|
|
updateCustomNonce,
|
|
} = this.props;
|
|
|
|
this._removeBeforeUnload();
|
|
updateCustomNonce('');
|
|
await cancelTransaction(txData);
|
|
history.push(mostRecentOverviewPage);
|
|
}
|
|
|
|
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,
|
|
addToAddressBookIfNew,
|
|
toAccounts,
|
|
toAddress,
|
|
} = this.props;
|
|
|
|
updateTxData({
|
|
txData,
|
|
maxFeePerGas,
|
|
customTokenAmount,
|
|
dappProposedTokenAmount,
|
|
currentTokenBalance,
|
|
maxPriorityFeePerGas,
|
|
baseFeePerGas,
|
|
addToAddressBookIfNew,
|
|
toAccounts,
|
|
toAddress,
|
|
name: methodData.name,
|
|
});
|
|
|
|
this.setState(
|
|
{
|
|
submitting: true,
|
|
submitError: null,
|
|
},
|
|
() => {
|
|
this._removeBeforeUnload();
|
|
|
|
sendTransaction(txData)
|
|
.then(() => {
|
|
if (!this._isMounted) {
|
|
return;
|
|
}
|
|
|
|
this.setState(
|
|
{
|
|
submitting: false,
|
|
},
|
|
() => {
|
|
history.push(mostRecentOverviewPage);
|
|
updateCustomNonce('');
|
|
},
|
|
);
|
|
})
|
|
.catch((error) => {
|
|
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,
|
|
showCustodianDeepLink,
|
|
clearConfirmTransaction,
|
|
} = 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 (txData.custodyStatus) {
|
|
showCustodianDeepLink({
|
|
fromAddress,
|
|
closeNotification: isNotification && unapprovedTxCount === 1,
|
|
txId: txData.id,
|
|
onDeepLinkFetched: () => {
|
|
this.context.trackEvent({
|
|
category: 'MMI',
|
|
event: 'Show deeplink for transaction',
|
|
});
|
|
},
|
|
onDeepLinkShown: () => {
|
|
clearConfirmTransaction();
|
|
if (!this._isMounted) {
|
|
return;
|
|
}
|
|
this.setState({ submitting: false }, () => {
|
|
history.push(mostRecentOverviewPage);
|
|
updateCustomNonce('');
|
|
});
|
|
},
|
|
});
|
|
} else {
|
|
if (!this._isMounted) {
|
|
return;
|
|
}
|
|
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('');
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
handleSetApprovalForAll() {
|
|
this.setState({ showWarningModal: true });
|
|
}
|
|
|
|
renderTitleComponent() {
|
|
const { title, hexTransactionAmount, txData } = this.props;
|
|
|
|
return (
|
|
<ConfirmTitle
|
|
title={title}
|
|
hexTransactionAmount={hexTransactionAmount}
|
|
txData={txData}
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderSubtitleComponent() {
|
|
const { assetStandard, subtitleComponent, hexTransactionAmount, txData } =
|
|
this.props;
|
|
|
|
return (
|
|
<ConfirmSubTitle
|
|
hexTransactionAmount={hexTransactionAmount}
|
|
subtitleComponent={subtitleComponent}
|
|
txData={txData}
|
|
assetStandard={assetStandard}
|
|
/>
|
|
);
|
|
}
|
|
|
|
_beforeUnloadForGasPolling = () => {
|
|
this._isMounted = false;
|
|
if (this.state.pollingToken) {
|
|
disconnectGasFeeEstimatePoller(this.state.pollingToken);
|
|
removePollingTokenFromAppState(this.state.pollingToken);
|
|
}
|
|
};
|
|
|
|
_removeBeforeUnload = () => {
|
|
window.removeEventListener('beforeunload', this._beforeUnloadForGasPolling);
|
|
};
|
|
|
|
componentDidMount() {
|
|
this._isMounted = true;
|
|
const {
|
|
toAddress,
|
|
txData: { origin } = {},
|
|
getNextNonce,
|
|
tryReverseResolveAddress,
|
|
} = this.props;
|
|
const { trackEvent } = this.context;
|
|
trackEvent({
|
|
category: MetaMetricsEventCategory.Transactions,
|
|
event: 'Confirm: Started',
|
|
properties: {
|
|
action: 'Confirm Screen',
|
|
legacy_event: true,
|
|
origin,
|
|
},
|
|
});
|
|
|
|
getNextNonce();
|
|
if (toAddress) {
|
|
tryReverseResolveAddress(toAddress);
|
|
}
|
|
|
|
/**
|
|
* This makes a request to get estimates and begin polling, keeping track of the poll
|
|
* token in component state.
|
|
* It then disconnects polling upon componentWillUnmount. If the hook is unmounted
|
|
* while waiting for `getGasFeeEstimatesAndStartPolling` to resolve, the `_isMounted`
|
|
* flag ensures that a call to disconnect happens after promise resolution.
|
|
*/
|
|
getGasFeeEstimatesAndStartPolling().then((pollingToken) => {
|
|
if (this._isMounted) {
|
|
addPollingTokenToAppState(pollingToken);
|
|
this.setState({ pollingToken });
|
|
} else {
|
|
disconnectGasFeeEstimatePoller(pollingToken);
|
|
removePollingTokenFromAppState(this.state.pollingToken);
|
|
}
|
|
});
|
|
window.addEventListener('beforeunload', this._beforeUnloadForGasPolling);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
this._beforeUnloadForGasPolling();
|
|
this._removeBeforeUnload();
|
|
this.props.clearConfirmTransaction();
|
|
}
|
|
|
|
supportsEIP1559 =
|
|
this.props.supportsEIP1559 && !isLegacyTransaction(this.props.txData);
|
|
|
|
render() {
|
|
const { t } = this.context;
|
|
const {
|
|
fromName,
|
|
fromAddress,
|
|
toName,
|
|
toAddress,
|
|
toEns,
|
|
toNickname,
|
|
methodData,
|
|
tokenAddress,
|
|
contentComponent,
|
|
onEdit,
|
|
nonce,
|
|
customNonceValue,
|
|
unapprovedTxCount,
|
|
type,
|
|
hideSenderToRecipient,
|
|
showAccountInHeader,
|
|
txData,
|
|
gasIsLoading,
|
|
gasFeeIsCustom,
|
|
nativeCurrency,
|
|
hardwareWalletRequiresConnection,
|
|
image,
|
|
isApprovalOrRejection,
|
|
assetStandard,
|
|
displayAccountBalanceHeader,
|
|
title,
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
isNoteToTraderSupported,
|
|
///: END:ONLY_INCLUDE_IN
|
|
} = this.props;
|
|
const {
|
|
submitting,
|
|
submitError,
|
|
submitWarning,
|
|
ethGasPriceWarning,
|
|
editingGas,
|
|
userAcknowledgedGasMissing,
|
|
showWarningModal,
|
|
} = this.state;
|
|
const { name } = methodData;
|
|
const { valid, errorKey } = this.getErrorKey();
|
|
const hasSimulationError = Boolean(txData.simulationFails);
|
|
const renderSimulationFailureWarning =
|
|
hasSimulationError && !userAcknowledgedGasMissing;
|
|
|
|
// This `isTokenApproval` case is added to handle possible rendering of this component from
|
|
// confirm-approve.js when `assetStandard` is `undefined`. That will happen if the request to
|
|
// get the asset standard fails. In that scenario, confirm-approve.js returns the `<ConfirmContractInteraction />`
|
|
// component, which in turn returns this `<ConfirmTransactionBase />` component. We meed to prevent
|
|
// the user from editing the transaction in those cases.
|
|
|
|
// as this component is made functional, useTransactionFunctionType can be used to get functionType
|
|
const isTokenApproval =
|
|
txData.type === TransactionType.tokenMethodSetApprovalForAll ||
|
|
txData.type === TransactionType.tokenMethodApprove;
|
|
|
|
const isContractInteraction =
|
|
txData.type === TransactionType.contractInteraction;
|
|
|
|
const isContractInteractionFromDapp =
|
|
(isTokenApproval || isContractInteraction) &&
|
|
txData.origin !== 'metamask';
|
|
let functionType;
|
|
if (isContractInteractionFromDapp) {
|
|
functionType = getMethodName(name);
|
|
}
|
|
|
|
if (!functionType) {
|
|
if (type) {
|
|
functionType = getTransactionTypeTitle(t, type, nativeCurrency);
|
|
} else {
|
|
functionType = t('contractInteraction');
|
|
}
|
|
}
|
|
|
|
return (
|
|
<TransactionModalContextProvider>
|
|
<ConfirmPageContainer
|
|
fromName={fromName}
|
|
fromAddress={fromAddress}
|
|
showAccountInHeader={showAccountInHeader}
|
|
toName={toName}
|
|
toAddress={toAddress}
|
|
toEns={toEns}
|
|
toNickname={toNickname}
|
|
showEdit={!isContractInteractionFromDapp && Boolean(onEdit)}
|
|
action={functionType}
|
|
image={image}
|
|
title={title}
|
|
titleComponent={this.renderTitleComponent()}
|
|
subtitleComponent={this.renderSubtitleComponent()}
|
|
detailsComponent={this.renderDetails()}
|
|
dataComponent={this.renderData(functionType)}
|
|
dataHexComponent={this.renderDataHex(functionType)}
|
|
contentComponent={contentComponent}
|
|
///: BEGIN:ONLY_INCLUDE_IN(build-mmi)
|
|
noteComponent={
|
|
isNoteToTraderSupported && (
|
|
<NoteToTrader
|
|
maxLength="280"
|
|
placeholder={t('notePlaceholder')}
|
|
onChange={(value) => this.setState({ noteText: value })}
|
|
noteText={this.state.noteText}
|
|
labelText={t('transactionNote')}
|
|
/>
|
|
)
|
|
}
|
|
///: END:ONLY_INCLUDE_IN
|
|
nonce={customNonceValue || nonce}
|
|
unapprovedTxCount={unapprovedTxCount}
|
|
tokenAddress={tokenAddress}
|
|
errorMessage={submitError}
|
|
errorKey={errorKey}
|
|
hasSimulationError={hasSimulationError}
|
|
warning={submitWarning}
|
|
disabled={
|
|
renderSimulationFailureWarning ||
|
|
!valid ||
|
|
submitting ||
|
|
hardwareWalletRequiresConnection ||
|
|
(gasIsLoading && !gasFeeIsCustom)
|
|
}
|
|
onEdit={() => this.handleEdit()}
|
|
onCancelAll={() => this.handleCancelAll()}
|
|
onCancel={() => this.handleCancel()}
|
|
onSubmit={() => this.handleSubmit()}
|
|
onSetApprovalForAll={() => this.handleSetApprovalForAll()}
|
|
showWarningModal={showWarningModal}
|
|
hideSenderToRecipient={hideSenderToRecipient}
|
|
origin={txData.origin}
|
|
ethGasPriceWarning={ethGasPriceWarning}
|
|
editingGas={editingGas}
|
|
handleCloseEditGas={() => this.handleCloseEditGas()}
|
|
currentTransaction={txData}
|
|
supportsEIP1559={this.supportsEIP1559}
|
|
nativeCurrency={nativeCurrency}
|
|
isApprovalOrRejection={isApprovalOrRejection}
|
|
assetStandard={assetStandard}
|
|
txData={txData}
|
|
displayAccountBalanceHeader={displayAccountBalanceHeader}
|
|
/>
|
|
</TransactionModalContextProvider>
|
|
);
|
|
}
|
|
}
|