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, { ConfirmDetailRow, } from '../../components/app/confirm-page-container'; import { isBalanceSufficient } from '../send/send.utils'; import { getHexGasTotal } from '../../helpers/utils/confirm-tx.util'; import { addHexes, hexToDecimal } 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 AdvancedGasInputs from '../../components/app/gas-customization/advanced-gas-inputs'; import TextField from '../../components/ui/text-field'; import AdvancedGasControls from '../../components/app/advanced-gas-controls'; import { TRANSACTION_TYPES, TRANSACTION_STATUSES, } from '../../../shared/constants/transaction'; import { getTransactionTypeTitle } from '../../helpers/utils/transactions.util'; import ErrorMessage from '../../components/ui/error-message'; 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'; 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, hexTransactionFee: 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, showCustomizeGasModal: 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, updateGasAndCalculate: PropTypes.func, customGas: PropTypes.object, // Component props actionKey: PropTypes.string, contentComponent: PropTypes.node, dataComponent: PropTypes.node, primaryTotalTextOverride: PropTypes.oneOfType([ PropTypes.string, PropTypes.node, ]), secondaryTotalTextOverride: PropTypes.string, hideData: PropTypes.bool, hideSubtitle: PropTypes.bool, identiconAddress: PropTypes.string, onEdit: PropTypes.func, subtitleComponent: PropTypes.node, title: PropTypes.string, advancedInlineGasShown: PropTypes.bool, insufficientBalance: PropTypes.bool, hideFiatConversion: PropTypes.bool, type: PropTypes.string, getNextNonce: PropTypes.func, nextNonce: PropTypes.number, tryReverseResolveAddress: PropTypes.func.isRequired, hideSenderToRecipient: PropTypes.bool, showAccountInHeader: PropTypes.bool, mostRecentOverviewPage: PropTypes.string.isRequired, isMainnet: PropTypes.bool, isEthGasPrice: PropTypes.bool, noGasPrice: PropTypes.bool, setDefaultHomeActiveTabName: PropTypes.func, }; 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, hexTransactionFee, txData: { simulationFails, txParams: { value: amount } = {} } = {}, customGas, noGasPrice, } = this.props; const insufficientBalance = balance && !isBalanceSufficient({ amount, gasTotal: hexTransactionFee || '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 { showCustomizeGasModal, 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, }, }); if (process.env.SHOW_EIP_1559_UI) { this.setState({ editingGas: true }); } else { showCustomizeGasModal(); } } // TODO: rename this 'handleCloseEditGas' later when we remove the // SHOW_EIP_1559_UI flag/branch handleCloseNewGasPopover() { this.setState({ editingGas: false }); } renderDetails() { const { primaryTotalTextOverride, secondaryTotalTextOverride, hexTransactionFee, hexTransactionTotal, useNonceField, customNonceValue, updateCustomNonce, advancedInlineGasShown, customGas, insufficientBalance, updateGasAndCalculate, hideFiatConversion, nextNonce, getNextNonce, isMainnet, isEthGasPrice, noGasPrice, txData, } = this.props; const { t } = this.context; const notMainnetOrTest = !(isMainnet || process.env.IN_TEST); const gasPriceFetchFailure = isEthGasPrice || noGasPrice; const inlineGasControls = process.env.SHOW_EIP_1559_UI ? ( ) : ( updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice }) } updateCustomGasLimit={(newGasLimit) => updateGasAndCalculate({ ...customGas, gasLimit: newGasLimit }) } customGasPrice={customGas.gasPrice} customGasLimit={customGas.gasLimit} insufficientBalance={insufficientBalance} customPriceIsSafe isSpeedUp={false} /> ); const nonceField = useNonceField ? ( {t('nonceFieldHeading')} { if (!value.length || Number(value) < 0) { updateCustomNonce(''); } else { updateCustomNonce(String(Math.floor(value))); } getNextNonce(); }} fullWidth margin="dense" value={customNonceValue || ''} /> ) : null; const showInlineControls = process.env.SHOW_EIP_1559_UI ? advancedInlineGasShown : advancedInlineGasShown || notMainnetOrTest || gasPriceFetchFailure; const showGasEditButton = process.env.SHOW_EIP_1559_UI ? !showInlineControls : !(notMainnetOrTest || gasPriceFetchFailure); if (process.env.SHOW_EIP_1559_UI) { return ( this.handleEditGas()} rows={[ {t('transactionDetailDappGasHeading', [txData.origin])} > ) : ( <> {t('transactionDetailGasHeading')} {t('transactionDetailGasTooltipIntro')} {t('transactionDetailGasTooltipExplanation')} {t('transactionDetailGasTooltipConversion')} > } position="top" > > ) } detailTitleColor={ txData.dappSuggestedGasFees ? COLORS.SECONDARY1 : COLORS.BLACK } detailText={ } detailTotal={ } subText={t('editGasSubTextFee', [ , ])} subTitle={ } />, } detailTotal={ } subTitle={t('transactionDetailGasTotalSubtitle')} subText={t('editGasSubTextAmount', [ , ])} />, ]} /> {nonceField} ); } return ( this.handleEditGas() : null } secondaryText={ hideFiatConversion ? t('noConversionRateAvailable') : '' } /> {showInlineControls ? inlineGasControls : null} {noGasPrice ? ( ) : null} {nonceField} ); } renderData(functionType) { const { t } = this.context; const { txData: { txParams: { data } = {} } = {}, methodData: { params } = {}, hideData, dataComponent, } = this.props; if (hideData) { return null; } return ( dataComponent || ( {`${t('functionType')}:`} {functionType} {params && ( {`${t('parameters')}:`} {JSON.stringify(params, null, 2)} )} {`${t('hexData')}: ${toBuffer(data).length} bytes`} {data} ) ); } handleEdit() { const { txData, tokenData, tokenProps, onEdit, actionKey, txData: { origin }, methodData = {}, } = this.props; this.context.metricsEvent({ eventOpts: { category: 'Transactions', action: 'Confirm Screen', name: 'Edit Transaction', }, customVariables: { recipientKnown: null, functionType: actionKey || getMethodName(methodData.name) || TRANSACTION_TYPES.CONTRACT_INTERACTION, 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); }, }); } handleCancel() { const { txData, cancelTransaction, history, mostRecentOverviewPage, clearConfirmTransaction, updateCustomNonce, } = this.props; this._removeBeforeUnload(); updateCustomNonce(''); cancelTransaction(txData).then(() => { clearConfirmTransaction(); history.push(mostRecentOverviewPage); }); } handleSubmit() { const { sendTransaction, clearConfirmTransaction, txData, history, mostRecentOverviewPage, updateCustomNonce, } = this.props; const { submitting } = this.state; if (submitting) { return; } this.setState( { submitting: true, submitError: null, }, () => { this._removeBeforeUnload(); sendTransaction(txData) .then(() => { clearConfirmTransaction(); this.setState( { submitting: false, }, () => { history.push(mostRecentOverviewPage); updateCustomNonce(''); }, ); }) .catch((error) => { this.setState({ submitting: false, submitError: error.message, }); updateCustomNonce(''); }); }, ); } renderTitleComponent() { const { title, hexTransactionAmount } = this.props; // Title string passed in by props takes priority if (title) { return null; } return ( ); } renderSubtitleComponent() { const { subtitleComponent, hexTransactionAmount } = this.props; return ( subtitleComponent || ( ) ); } handleNextTx(txId) { const { history, clearConfirmTransaction } = this.props; if (txId) { clearConfirmTransaction(); history.push(`${CONFIRM_TRANSACTION_ROUTE}/${txId}`); } } getNavigateTxData() { const { currentNetworkUnapprovedTxs, txData: { id } = {} } = this.props; const enumUnapprovedTxs = Object.keys(currentNetworkUnapprovedTxs); const currentPosition = enumUnapprovedTxs.indexOf(id ? id.toString() : ''); return { totalTx: enumUnapprovedTxs.length, positionOfCurrentTx: currentPosition + 1, nextTxId: enumUnapprovedTxs[currentPosition + 1], prevTxId: enumUnapprovedTxs[currentPosition - 1], showNavigation: enumUnapprovedTxs.length > 1, firstTx: enumUnapprovedTxs[0], lastTx: enumUnapprovedTxs[enumUnapprovedTxs.length - 1], ofText: this.context.t('ofTextNofM'), requestsWaitingText: this.context.t('requestsAwaitingAcknowledgement'), }; } _beforeUnload = () => { const { txData: { id } = {}, cancelTransaction } = this.props; cancelTransaction({ id }); }; _removeBeforeUnload = () => { if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { window.removeEventListener('beforeunload', this._beforeUnload); } }; componentDidMount() { const { toAddress, txData: { origin } = {}, getNextNonce, tryReverseResolveAddress, } = this.props; const { metricsEvent } = this.context; metricsEvent({ eventOpts: { category: 'Transactions', action: 'Confirm Screen', name: 'Confirm: Started', }, customVariables: { origin, }, }); if (getEnvironmentType() === ENVIRONMENT_TYPE_NOTIFICATION) { window.addEventListener('beforeunload', this._beforeUnload); } getNextNonce(); if (toAddress) { tryReverseResolveAddress(toAddress); } } componentWillUnmount() { this._removeBeforeUnload(); } render() { const { t } = this.context; const { fromName, fromAddress, toName, toAddress, toEns, toNickname, methodData, title, hideSubtitle, identiconAddress, contentComponent, onEdit, nonce, customNonceValue, assetImage, unapprovedTxCount, type, hideSenderToRecipient, showAccountInHeader, txData, } = this.props; const { submitting, submitError, submitWarning, ethGasPriceWarning, editingGas, } = this.state; const { name } = methodData; const { valid, errorKey } = this.getErrorKey(); const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText, } = this.getNavigateTxData(); let functionType = getMethodName(name); if (!functionType) { if (type) { functionType = getTransactionTypeTitle(t, type); } else { functionType = t('contractInteraction'); } } return ( this.handleNextTx(txId)} firstTx={firstTx} lastTx={lastTx} ofText={ofText} requestsWaitingText={requestsWaitingText} disabled={!valid || submitting} onEdit={() => this.handleEdit()} onCancelAll={() => this.handleCancelAll()} onCancel={() => this.handleCancel()} onSubmit={() => this.handleSubmit()} hideSenderToRecipient={hideSenderToRecipient} origin={txData.origin} ethGasPriceWarning={ethGasPriceWarning} editingGas={editingGas} handleCloseEditGas={() => this.handleCloseNewGasPopover()} currentTransaction={txData} /> ); } } export function getMethodName(camelCase) { if (!camelCase || typeof camelCase !== 'string') { return ''; } return camelCase .replace(/([a-z])([A-Z])/gu, '$1 $2') .replace(/([A-Z])([a-z])/gu, ' $1$2') .replace(/ +/gu, ' '); }
{t('transactionDetailGasTooltipIntro')}
{t('transactionDetailGasTooltipExplanation')}
{t('transactionDetailGasTooltipConversion')}
{JSON.stringify(params, null, 2)}