aboutsummaryrefslogtreecommitdiffstats
path: root/ui/app/pages/confirm-transaction-base
diff options
context:
space:
mode:
Diffstat (limited to 'ui/app/pages/confirm-transaction-base')
-rw-r--r--ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js574
-rw-r--r--ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js242
-rw-r--r--ui/app/pages/confirm-transaction-base/index.js1
-rw-r--r--ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js14
4 files changed, 831 insertions, 0 deletions
diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
new file mode 100644
index 000000000..1da9c34bd
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.component.js
@@ -0,0 +1,574 @@
+import ethUtil from 'ethereumjs-util'
+import React, { Component } from 'react'
+import PropTypes from 'prop-types'
+import ConfirmPageContainer, { ConfirmDetailRow } from '../../components/app/confirm-page-container'
+import { isBalanceSufficient } from '../../components/app/send/send.utils'
+import { DEFAULT_ROUTE, CONFIRM_TRANSACTION_ROUTE } from '../../helpers/constants/routes'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ TRANSACTION_ERROR_KEY,
+} from '../../helpers/constants/error-keys'
+import { CONFIRMED_STATUS, DROPPED_STATUS } from '../../helpers/constants/transactions'
+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'
+
+export default class ConfirmTransactionBase extends Component {
+ static contextTypes = {
+ t: PropTypes.func,
+ metricsEvent: PropTypes.func,
+ }
+
+ static propTypes = {
+ // react-router props
+ match: PropTypes.object,
+ history: PropTypes.object,
+ // Redux props
+ balance: PropTypes.string,
+ cancelTransaction: PropTypes.func,
+ cancelAllTransactions: PropTypes.func,
+ clearConfirmTransaction: PropTypes.func,
+ clearSend: PropTypes.func,
+ conversionRate: PropTypes.number,
+ currentCurrency: PropTypes.string,
+ editTransaction: PropTypes.func,
+ ethTransactionAmount: PropTypes.string,
+ ethTransactionFee: PropTypes.string,
+ ethTransactionTotal: PropTypes.string,
+ fiatTransactionAmount: PropTypes.string,
+ fiatTransactionFee: PropTypes.string,
+ fiatTransactionTotal: PropTypes.string,
+ fromAddress: PropTypes.string,
+ fromName: PropTypes.string,
+ hexTransactionAmount: PropTypes.string,
+ hexTransactionFee: PropTypes.string,
+ hexTransactionTotal: PropTypes.string,
+ isTxReprice: PropTypes.bool,
+ methodData: PropTypes.object,
+ nonce: PropTypes.string,
+ 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,
+ transactionStatus: PropTypes.string,
+ txData: PropTypes.object,
+ unapprovedTxCount: PropTypes.number,
+ currentNetworkUnapprovedTxs: PropTypes.object,
+ updateGasAndCalculate: PropTypes.func,
+ customGas: PropTypes.object,
+ // Component props
+ action: PropTypes.string,
+ contentComponent: PropTypes.node,
+ dataComponent: PropTypes.node,
+ detailsComponent: PropTypes.node,
+ errorKey: PropTypes.string,
+ errorMessage: PropTypes.string,
+ primaryTotalTextOverride: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
+ secondaryTotalTextOverride: PropTypes.string,
+ hideData: PropTypes.bool,
+ hideDetails: PropTypes.bool,
+ hideSubtitle: PropTypes.bool,
+ identiconAddress: PropTypes.string,
+ onCancel: PropTypes.func,
+ onEdit: PropTypes.func,
+ onEditGas: PropTypes.func,
+ onSubmit: PropTypes.func,
+ setMetaMetricsSendCount: PropTypes.func,
+ metaMetricsSendCount: PropTypes.number,
+ subtitle: PropTypes.string,
+ subtitleComponent: PropTypes.node,
+ summaryComponent: PropTypes.node,
+ title: PropTypes.string,
+ titleComponent: PropTypes.node,
+ valid: PropTypes.bool,
+ warning: PropTypes.string,
+ advancedInlineGasShown: PropTypes.bool,
+ insufficientBalance: PropTypes.bool,
+ hideFiatConversion: PropTypes.bool,
+ }
+
+ state = {
+ submitting: false,
+ submitError: null,
+ }
+
+ componentDidUpdate () {
+ const {
+ transactionStatus,
+ showTransactionConfirmedModal,
+ history,
+ clearConfirmTransaction,
+ } = this.props
+
+ if (transactionStatus === DROPPED_STATUS || transactionStatus === CONFIRMED_STATUS) {
+ showTransactionConfirmedModal({
+ onSubmit: () => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ },
+ })
+
+ return
+ }
+ }
+
+ getErrorKey () {
+ const {
+ balance,
+ conversionRate,
+ hexTransactionFee,
+ txData: {
+ simulationFails,
+ txParams: {
+ value: amount,
+ } = {},
+ } = {},
+ } = this.props
+
+ const insufficientBalance = balance && !isBalanceSufficient({
+ amount,
+ gasTotal: hexTransactionFee || '0x0',
+ balance,
+ conversionRate,
+ })
+
+ if (insufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ if (simulationFails) {
+ return {
+ valid: true,
+ errorKey: simulationFails.errorKey ? simulationFails.errorKey : TRANSACTION_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+
+ handleEditGas () {
+ const { onEditGas, showCustomizeGasModal, action, txData: { origin }, methodData = {} } = this.props
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'User clicks "Edit" on gas',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ if (onEditGas) {
+ onEditGas()
+ } else {
+ showCustomizeGasModal()
+ }
+ }
+
+ renderDetails () {
+ const {
+ detailsComponent,
+ primaryTotalTextOverride,
+ secondaryTotalTextOverride,
+ hexTransactionFee,
+ hexTransactionTotal,
+ hideDetails,
+ advancedInlineGasShown,
+ customGas,
+ insufficientBalance,
+ updateGasAndCalculate,
+ hideFiatConversion,
+ } = this.props
+
+ if (hideDetails) {
+ return null
+ }
+
+ return (
+ detailsComponent || (
+ <div className="confirm-page-container-content__details">
+ <div className="confirm-page-container-content__gas-fee">
+ <ConfirmDetailRow
+ label="Gas Fee"
+ value={hexTransactionFee}
+ headerText="Edit"
+ headerTextClassName="confirm-detail-row__header-text--edit"
+ onHeaderClick={() => this.handleEditGas()}
+ secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : ''}
+ />
+ {advancedInlineGasShown
+ ? <AdvancedGasInputs
+ updateCustomGasPrice={newGasPrice => updateGasAndCalculate({ ...customGas, gasPrice: newGasPrice })}
+ updateCustomGasLimit={newGasLimit => updateGasAndCalculate({ ...customGas, gasLimit: newGasLimit })}
+ customGasPrice={customGas.gasPrice}
+ customGasLimit={customGas.gasLimit}
+ insufficientBalance={insufficientBalance}
+ customPriceIsSafe={true}
+ isSpeedUp={false}
+ />
+ : null
+ }
+ </div>
+ <div>
+ <ConfirmDetailRow
+ label="Total"
+ value={hexTransactionTotal}
+ primaryText={primaryTotalTextOverride}
+ secondaryText={hideFiatConversion ? this.context.t('noConversionRateAvailable') : secondaryTotalTextOverride}
+ headerText="Amount + Gas Fee"
+ headerTextClassName="confirm-detail-row__header-text--total"
+ primaryValueTextColor="#2f9ae0"
+ />
+ </div>
+ </div>
+ )
+ )
+ }
+
+ renderData () {
+ const { t } = this.context
+ const {
+ txData: {
+ txParams: {
+ data,
+ } = {},
+ } = {},
+ methodData: {
+ name,
+ params,
+ } = {},
+ hideData,
+ dataComponent,
+ } = this.props
+
+ if (hideData) {
+ return null
+ }
+
+ return dataComponent || (
+ <div className="confirm-page-container-content__data">
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('functionType')}:`}
+ <span className="confirm-page-container-content__function-type">
+ { name || t('notFound') }
+ </span>
+ </div>
+ {
+ params && (
+ <div className="confirm-page-container-content__data-box">
+ <div className="confirm-page-container-content__data-field-label">
+ { `${t('parameters')}:` }
+ </div>
+ <div>
+ <pre>{ JSON.stringify(params, null, 2) }</pre>
+ </div>
+ </div>
+ )
+ }
+ <div className="confirm-page-container-content__data-box-label">
+ {`${t('hexData')}: ${ethUtil.toBuffer(data).length} bytes`}
+ </div>
+ <div className="confirm-page-container-content__data-box">
+ { data }
+ </div>
+ </div>
+ )
+ }
+
+ handleEdit () {
+ const { txData, tokenData, tokenProps, onEdit, action, txData: { origin }, methodData = {} } = this.props
+
+ this.context.metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Edit Transaction',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ onEdit({ txData, tokenData, tokenProps })
+ }
+
+ handleCancelAll () {
+ const {
+ cancelAllTransactions,
+ clearConfirmTransaction,
+ history,
+ showRejectTransactionsConfirmationModal,
+ unapprovedTxCount,
+ } = this.props
+
+ showRejectTransactionsConfirmationModal({
+ unapprovedTxCount,
+ async onSubmit () {
+ await cancelAllTransactions()
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ },
+ })
+ }
+
+ handleCancel () {
+ const { metricsEvent } = this.context
+ const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, action, txData: { origin }, methodData = {} } = this.props
+
+ if (onCancel) {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Cancel',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+ onCancel(txData)
+ } else {
+ cancelTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ history.push(DEFAULT_ROUTE)
+ })
+ }
+ }
+
+ handleSubmit () {
+ const { metricsEvent } = this.context
+ const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, action, metaMetricsSendCount = 0, setMetaMetricsSendCount, methodData = {} } = this.props
+ const { submitting } = this.state
+
+ if (submitting) {
+ return
+ }
+
+ this.setState({
+ submitting: true,
+ submitError: null,
+ }, () => {
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Transaction Completed',
+ },
+ customVariables: {
+ recipientKnown: null,
+ functionType: action || getMethodName(methodData.name) || this.context.t('contractInteraction'),
+ origin,
+ },
+ })
+
+ setMetaMetricsSendCount(metaMetricsSendCount + 1)
+ .then(() => {
+ if (onSubmit) {
+ Promise.resolve(onSubmit(txData))
+ .then(() => {
+ this.setState({
+ submitting: false,
+ })
+ })
+ } else {
+ sendTransaction(txData)
+ .then(() => {
+ clearConfirmTransaction()
+ this.setState({
+ submitting: false,
+ }, () => {
+ history.push(DEFAULT_ROUTE)
+ })
+ })
+ .catch(error => {
+ this.setState({
+ submitting: false,
+ submitError: error.message,
+ })
+ })
+ }
+ })
+ })
+ }
+
+ renderTitleComponent () {
+ const { title, titleComponent, hexTransactionAmount } = this.props
+
+ // Title string passed in by props takes priority
+ if (title) {
+ return null
+ }
+
+ return titleComponent || (
+ <UserPreferencedCurrencyDisplay
+ value={hexTransactionAmount}
+ type={PRIMARY}
+ showEthLogo
+ ethLogoHeight="26"
+ hideLabel
+ />
+ )
+ }
+
+ renderSubtitleComponent () {
+ const { subtitle, subtitleComponent, hexTransactionAmount } = this.props
+
+ // Subtitle string passed in by props takes priority
+ if (subtitle) {
+ return null
+ }
+
+ return subtitleComponent || (
+ <UserPreferencedCurrencyDisplay
+ value={hexTransactionAmount}
+ type={SECONDARY}
+ showEthLogo
+ hideLabel
+ />
+ )
+ }
+
+ 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).reverse()
+ const currentPosition = enumUnapprovedTxs.indexOf(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'),
+ }
+ }
+
+ componentDidMount () {
+ const { txData: { origin } = {} } = this.props
+ const { metricsEvent } = this.context
+ metricsEvent({
+ eventOpts: {
+ category: 'Transactions',
+ action: 'Confirm Screen',
+ name: 'Confirm: Started',
+ },
+ customVariables: {
+ origin,
+ },
+ })
+ }
+
+ render () {
+ const {
+ isTxReprice,
+ fromName,
+ fromAddress,
+ toName,
+ toAddress,
+ methodData,
+ valid: propsValid = true,
+ errorMessage,
+ errorKey: propsErrorKey,
+ action,
+ title,
+ subtitle,
+ hideSubtitle,
+ identiconAddress,
+ summaryComponent,
+ contentComponent,
+ onEdit,
+ nonce,
+ assetImage,
+ warning,
+ unapprovedTxCount,
+ } = this.props
+ const { submitting, submitError } = this.state
+
+ const { name } = methodData
+ const { valid, errorKey } = this.getErrorKey()
+ const { totalTx, positionOfCurrentTx, nextTxId, prevTxId, showNavigation, firstTx, lastTx, ofText, requestsWaitingText } = this.getNavigateTxData()
+
+ return (
+ <ConfirmPageContainer
+ fromName={fromName}
+ fromAddress={fromAddress}
+ toName={toName}
+ toAddress={toAddress}
+ showEdit={onEdit && !isTxReprice}
+ action={action || getMethodName(name) || this.context.t('contractInteraction')}
+ title={title}
+ titleComponent={this.renderTitleComponent()}
+ subtitle={subtitle}
+ subtitleComponent={this.renderSubtitleComponent()}
+ hideSubtitle={hideSubtitle}
+ summaryComponent={summaryComponent}
+ detailsComponent={this.renderDetails()}
+ dataComponent={this.renderData()}
+ contentComponent={contentComponent}
+ nonce={nonce}
+ unapprovedTxCount={unapprovedTxCount}
+ assetImage={assetImage}
+ identiconAddress={identiconAddress}
+ errorMessage={errorMessage || submitError}
+ errorKey={propsErrorKey || errorKey}
+ warning={warning}
+ totalTx={totalTx}
+ positionOfCurrentTx={positionOfCurrentTx}
+ nextTxId={nextTxId}
+ prevTxId={prevTxId}
+ showNavigation={showNavigation}
+ onNextTx={(txId) => this.handleNextTx(txId)}
+ firstTx={firstTx}
+ lastTx={lastTx}
+ ofText={ofText}
+ requestsWaitingText={requestsWaitingText}
+ disabled={!propsValid || !valid || submitting}
+ onEdit={() => this.handleEdit()}
+ onCancelAll={() => this.handleCancelAll()}
+ onCancel={() => this.handleCancel()}
+ onSubmit={() => this.handleSubmit()}
+ />
+ )
+ }
+}
+
+export function getMethodName (camelCase) {
+ if (!camelCase || typeof camelCase !== 'string') {
+ return ''
+ }
+
+ return camelCase
+ .replace(/([a-z])([A-Z])/g, '$1 $2')
+ .replace(/([A-Z])([a-z])/g, ' $1$2')
+ .replace(/ +/g, ' ')
+}
diff --git a/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
new file mode 100644
index 000000000..83543f1a4
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/confirm-transaction-base.container.js
@@ -0,0 +1,242 @@
+import { connect } from 'react-redux'
+import { compose } from 'recompose'
+import { withRouter } from 'react-router-dom'
+import R from 'ramda'
+import contractMap from 'eth-contract-metadata'
+import ConfirmTransactionBase from './confirm-transaction-base.component'
+import {
+ clearConfirmTransaction,
+ updateGasAndCalculate,
+} from '../../ducks/confirm-transaction/confirm-transaction.duck'
+import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal, setMetaMetricsSendCount } from '../../store/actions'
+import {
+ INSUFFICIENT_FUNDS_ERROR_KEY,
+ GAS_LIMIT_TOO_LOW_ERROR_KEY,
+} from '../../helpers/constants/error-keys'
+import { getHexGasTotal } from '../../helpers/utils/confirm-tx.util'
+import { isBalanceSufficient, calcGasTotal } from '../../components/app/send/send.utils'
+import { conversionGreaterThan } from '../../helpers/utils/conversion-util'
+import { MIN_GAS_LIMIT_DEC } from '../../components/app/send/send.constants'
+import { checksumAddress, addressSlicer, valuesFor } from '../../helpers/utils/util'
+import {getMetaMaskAccounts, getAdvancedInlineGasShown, preferencesSelector, getIsMainnet} from '../../selectors/selectors'
+
+const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
+ return {
+ ...acc,
+ [base.toLowerCase()]: contractMap[base],
+ }
+}, {})
+
+const mapStateToProps = (state, props) => {
+ const { toAddress: propsToAddress } = props
+ const { showFiatInTestnets } = preferencesSelector(state)
+ const isMainnet = getIsMainnet(state)
+ const { confirmTransaction, metamask, gas } = state
+ const {
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexTransactionAmount,
+ hexTransactionFee,
+ hexTransactionTotal,
+ tokenData,
+ methodData,
+ txData,
+ tokenProps,
+ nonce,
+ } = confirmTransaction
+ const { txParams = {}, lastGasPrice, id: transactionId } = txData
+ const {
+ from: fromAddress,
+ to: txParamsToAddress,
+ gasPrice,
+ gas: gasLimit,
+ value: amount,
+ } = txParams
+ const accounts = getMetaMaskAccounts(state)
+ const {
+ conversionRate,
+ identities,
+ currentCurrency,
+ selectedAddress,
+ selectedAddressTxList,
+ assetImages,
+ network,
+ unapprovedTxs,
+ metaMetricsSendCount,
+ } = metamask
+ const assetImage = assetImages[txParamsToAddress]
+
+ const {
+ customGasLimit,
+ customGasPrice,
+ } = gas
+
+ const { balance } = accounts[selectedAddress]
+ const { name: fromName } = identities[selectedAddress]
+ const toAddress = propsToAddress || txParamsToAddress
+ const toName = identities[toAddress]
+ ? identities[toAddress].name
+ : (
+ casedContractMap[toAddress]
+ ? casedContractMap[toAddress].name
+ : addressSlicer(checksumAddress(toAddress))
+ )
+
+ const isTxReprice = Boolean(lastGasPrice)
+
+ const transaction = R.find(({ id }) => id === transactionId)(selectedAddressTxList)
+ const transactionStatus = transaction ? transaction.status : ''
+
+ const currentNetworkUnapprovedTxs = R.filter(
+ ({ metamaskNetworkId }) => metamaskNetworkId === network,
+ unapprovedTxs,
+ )
+ const unapprovedTxCount = valuesFor(currentNetworkUnapprovedTxs).length
+
+ const insufficientBalance = !isBalanceSufficient({
+ amount,
+ gasTotal: calcGasTotal(gasLimit, gasPrice),
+ balance,
+ conversionRate,
+ })
+
+ return {
+ balance,
+ fromAddress,
+ fromName,
+ toAddress,
+ toName,
+ ethTransactionAmount,
+ ethTransactionFee,
+ ethTransactionTotal,
+ fiatTransactionAmount,
+ fiatTransactionFee,
+ fiatTransactionTotal,
+ hexTransactionAmount,
+ hexTransactionFee,
+ hexTransactionTotal,
+ txData,
+ tokenData,
+ methodData,
+ tokenProps,
+ isTxReprice,
+ currentCurrency,
+ conversionRate,
+ transactionStatus,
+ nonce,
+ assetImage,
+ unapprovedTxs,
+ unapprovedTxCount,
+ currentNetworkUnapprovedTxs,
+ customGas: {
+ gasLimit: customGasLimit || gasLimit,
+ gasPrice: customGasPrice || gasPrice,
+ },
+ advancedInlineGasShown: getAdvancedInlineGasShown(state),
+ insufficientBalance,
+ hideSubtitle: (!isMainnet && !showFiatInTestnets),
+ hideFiatConversion: (!isMainnet && !showFiatInTestnets),
+ metaMetricsSendCount,
+ }
+}
+
+const mapDispatchToProps = dispatch => {
+ return {
+ clearConfirmTransaction: () => dispatch(clearConfirmTransaction()),
+ clearSend: () => dispatch(clearSend()),
+ showTransactionConfirmedModal: ({ onSubmit }) => {
+ return dispatch(showModal({ name: 'TRANSACTION_CONFIRMED', onSubmit }))
+ },
+ showCustomizeGasModal: ({ txData, onSubmit, validate }) => {
+ return dispatch(showModal({ name: 'CUSTOMIZE_GAS', txData, onSubmit, validate }))
+ },
+ updateGasAndCalculate: ({ gasLimit, gasPrice }) => {
+ return dispatch(updateGasAndCalculate({ gasLimit, gasPrice }))
+ },
+ showRejectTransactionsConfirmationModal: ({ onSubmit, unapprovedTxCount }) => {
+ return dispatch(showModal({ name: 'REJECT_TRANSACTIONS', onSubmit, unapprovedTxCount }))
+ },
+ cancelTransaction: ({ id }) => dispatch(cancelTx({ id })),
+ cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)),
+ sendTransaction: txData => dispatch(updateAndApproveTx(txData)),
+ setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)),
+ }
+}
+
+const getValidateEditGas = ({ balance, conversionRate, txData }) => {
+ const { txParams: { value: amount } = {} } = txData
+
+ return ({ gasLimit, gasPrice }) => {
+ const gasTotal = getHexGasTotal({ gasLimit, gasPrice })
+ const hasSufficientBalance = isBalanceSufficient({
+ amount,
+ gasTotal,
+ balance,
+ conversionRate,
+ })
+
+ if (!hasSufficientBalance) {
+ return {
+ valid: false,
+ errorKey: INSUFFICIENT_FUNDS_ERROR_KEY,
+ }
+ }
+
+ const gasLimitTooLow = gasLimit && conversionGreaterThan(
+ {
+ value: MIN_GAS_LIMIT_DEC,
+ fromNumericBase: 'dec',
+ conversionRate,
+ },
+ {
+ value: gasLimit,
+ fromNumericBase: 'hex',
+ },
+ )
+
+ if (gasLimitTooLow) {
+ return {
+ valid: false,
+ errorKey: GAS_LIMIT_TOO_LOW_ERROR_KEY,
+ }
+ }
+
+ return {
+ valid: true,
+ }
+ }
+}
+
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const { balance, conversionRate, txData, unapprovedTxs } = stateProps
+ const {
+ cancelAllTransactions: dispatchCancelAllTransactions,
+ showCustomizeGasModal: dispatchShowCustomizeGasModal,
+ updateGasAndCalculate: dispatchUpdateGasAndCalculate,
+ ...otherDispatchProps
+ } = dispatchProps
+
+ const validateEditGas = getValidateEditGas({ balance, conversionRate, txData })
+
+ return {
+ ...stateProps,
+ ...otherDispatchProps,
+ ...ownProps,
+ showCustomizeGasModal: () => dispatchShowCustomizeGasModal({
+ txData,
+ onSubmit: customGas => dispatchUpdateGasAndCalculate(customGas),
+ validate: validateEditGas,
+ }),
+ cancelAllTransactions: () => dispatchCancelAllTransactions(valuesFor(unapprovedTxs)),
+ updateGasAndCalculate: dispatchUpdateGasAndCalculate,
+ }
+}
+
+export default compose(
+ withRouter,
+ connect(mapStateToProps, mapDispatchToProps, mergeProps)
+)(ConfirmTransactionBase)
diff --git a/ui/app/pages/confirm-transaction-base/index.js b/ui/app/pages/confirm-transaction-base/index.js
new file mode 100644
index 000000000..9996e9aeb
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/index.js
@@ -0,0 +1 @@
+export { default } from './confirm-transaction-base.container'
diff --git a/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js
new file mode 100644
index 000000000..8ca7ca4e7
--- /dev/null
+++ b/ui/app/pages/confirm-transaction-base/tests/confirm-transaction-base.component.test.js
@@ -0,0 +1,14 @@
+import assert from 'assert'
+import { getMethodName } from '../confirm-transaction-base.component'
+
+describe('ConfirmTransactionBase Component', () => {
+ describe('getMethodName', () => {
+ it('should get correct method names', () => {
+ assert.equal(getMethodName(undefined), '')
+ assert.equal(getMethodName({}), '')
+ assert.equal(getMethodName('confirm'), 'confirm')
+ assert.equal(getMethodName('balanceOf'), 'balance Of')
+ assert.equal(getMethodName('ethToTokenSwapInput'), 'eth To Token Swap Input')
+ })
+ })
+})