import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ENVIRONMENT_TYPE_NOTIFICATION } from '../../../shared/constants/app';
import { getEnvironmentType } from '../../../app/scripts/lib/util';
import ConfirmPageContainer from '../../components/app/confirm-page-container';
import { isBalanceSufficient } from '../send/send.utils';
import {
addHexes,
hexToDecimal,
hexWEIToDecGWEI,
} from '../../helpers/utils/conversions.util';
import {
CONFIRM_TRANSACTION_ROUTE,
DEFAULT_ROUTE,
} from '../../helpers/constants/routes';
import {
INSUFFICIENT_FUNDS_ERROR_KEY,
TRANSACTION_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 {
TRANSACTION_TYPES,
TRANSACTION_STATUSES,
} from '../../../shared/constants/transaction';
import { getTransactionTypeTitle } from '../../helpers/utils/transactions.util';
import { toBuffer } from '../../../shared/modules/buffer-utils';
import TransactionDetail from '../../components/app/transaction-detail/transaction-detail.component';
import TransactionDetailItem from '../../components/app/transaction-detail-item/transaction-detail-item.component';
import InfoTooltip from '../../components/ui/info-tooltip/info-tooltip';
import GasTiming from '../../components/app/gas-timing/gas-timing.component';
import { COLORS } from '../../helpers/constants/design-system';
import {
disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling,
addPollingTokenToAppState,
removePollingTokenFromAppState,
} from '../../store/actions';
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
t: PropTypes.func,
metricsEvent: 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,
hexMinimumTransactionFee: PropTypes.string,
hexMaximumTransactionFee: PropTypes.string,
hexTransactionTotal: PropTypes.string,
methodData: PropTypes.object,
nonce: PropTypes.string,
useNonceField: PropTypes.bool,
customNonceValue: PropTypes.string,
updateCustomNonce: PropTypes.func,
assetImage: PropTypes.string,
sendTransaction: PropTypes.func,
showTransactionConfirmedModal: PropTypes.func,
showRejectTransactionsConfirmationModal: PropTypes.func,
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,
currentNetworkUnapprovedTxs: PropTypes.object,
customGas: PropTypes.object,
// Component props
actionKey: PropTypes.string,
contentComponent: PropTypes.node,
dataComponent: PropTypes.node,
hideData: PropTypes.bool,
hideSubtitle: PropTypes.bool,
identiconAddress: PropTypes.string,
onEdit: PropTypes.func,
subtitleComponent: PropTypes.node,
title: 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,
};
state = {
submitting: false,
submitError: null,
submitWarning: '',
ethGasPriceWarning: '',
editingGas: false,
};
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 === TRANSACTION_STATUSES.DROPPED ||
transactionStatus === TRANSACTION_STATUSES.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: { simulationFails, txParams: { value: amount } = {} } = {},
customGas,
noGasPrice,
} = 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) < 21000) {
return {
valid: false,
errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
};
}
if (simulationFails) {
return {
valid: true,
errorKey: simulationFails.errorKey
? simulationFails.errorKey
: TRANSACTION_ERROR_KEY,
};
}
if (noGasPrice) {
return {
valid: false,
errorKey: GAS_PRICE_FETCH_FAILURE_ERROR_KEY,
};
}
return {
valid: true,
};
}
handleEditGas() {
const {
actionKey,
txData: { origin },
methodData = {},
} = this.props;
this.context.metricsEvent({
eventOpts: {
category: 'Transactions',
action: 'Confirm Screen',
name: 'User clicks "Edit" on gas',
},
customVariables: {
recipientKnown: null,
functionType:
actionKey ||
getMethodName(methodData.name) ||
TRANSACTION_TYPES.CONTRACT_INTERACTION,
origin,
},
});
this.setState({ editingGas: true });
}
handleCloseEditGas() {
this.setState({ editingGas: false });
}
renderDetails() {
const {
primaryTotalTextOverride,
secondaryTotalTextOverride,
hexMinimumTransactionFee,
hexMaximumTransactionFee,
hexTransactionTotal,
useNonceField,
customNonceValue,
updateCustomNonce,
nextNonce,
getNextNonce,
txData,
useNativeCurrencyAsPrimaryCurrency,
primaryTotalTextOverrideMaxAmount,
maxFeePerGas,
maxPriorityFeePerGas,
} = this.props;
const { t } = this.context;
const getRequestingOrigin = () => {
try {
return new URL(txData.origin)?.hostname;
} catch (err) {
return '';
}
};
const renderTotalMaxAmount = () => {
if (
primaryTotalTextOverrideMaxAmount === undefined &&
secondaryTotalTextOverride === undefined
) {
// Native Send
return (
{t('transactionDetailGasTooltipIntro')}
{t('transactionDetailGasTooltipExplanation')}
> } position="top" >{JSON.stringify(params, null, 2)}