Update send and confirm state management, and tx controller gas defaults, for EIP1559 (#11549)

wip

Documentation improvements for send slice support of EIP1559

Remove console.log in send duck

Property lookup safety improvement in selectors/confirm-transaction

Add code accidentally removed in rebase

Update addTxGasDefaults and _getDefaultGasFees to work with new estimate types, and ensure we correctly handle gas price estimates when on EIP1559 networks (#11615)

* Fix typo

Remove console.log in send duck

* Update addTxGasDefaults and _getDefaultGasFees to work correctly with all new gas fee estimate types

* Don't show gas timing support when not on eip1559 compatible network

* Hide gas timing component on transaction screen when on a non-1559 network

* Improve comments, tests and edge case handling

* Ensure eip1559 fees are applied and updated correctly when eip1559 estimate api fails

* Lint fix

Co-authored-by: Brad Decker <git@braddecker.dev>

Remove console.log

Handle possible gasEstimateType undefined

Remove unnecessary nonce field position change in confirm-page-container-content__details
feature/default_network_editable
Brad Decker 3 years ago committed by GitHub
parent f1140087b7
commit e0953d9f68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 127
      app/scripts/controllers/transactions/index.js
  2. 242
      app/scripts/controllers/transactions/index.test.js
  3. 3
      app/scripts/metamask-controller.js
  4. 5
      jest.config.js
  5. 3
      shared/modules/conversion.utils.js
  6. 39
      ui/components/app/advanced-gas-controls/advanced-gas-controls.component.js
  7. 6
      ui/components/app/edit-gas-display/edit-gas-display.component.js
  8. 38
      ui/components/app/edit-gas-popover/edit-gas-popover.component.js
  9. 245
      ui/ducks/send/send.js
  10. 747
      ui/ducks/send/send.test.js
  11. 34
      ui/hooks/useGasFeeEstimates.js
  12. 6
      ui/hooks/useGasFeeEstimates.test.js
  13. 17
      ui/hooks/useGasFeeInputs.js
  14. 19
      ui/hooks/useGasFeeInputs.test.js
  15. 33
      ui/hooks/useSafeGasEstimatePolling.js
  16. 39
      ui/pages/confirm-transaction-base/confirm-transaction-base.component.js
  17. 13
      ui/pages/confirm-transaction-base/confirm-transaction-base.container.js
  18. 5
      ui/pages/send/send-header/send-header.component.js
  19. 2
      ui/pages/send/send-header/send-header.component.test.js
  20. 100
      ui/selectors/confirm-transaction.js

@ -27,7 +27,11 @@ import {
TRANSACTION_ENVELOPE_TYPES,
} from '../../../../shared/constants/transaction';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import { GAS_LIMITS } from '../../../../shared/constants/gas';
import {
GAS_LIMITS,
GAS_ESTIMATE_TYPES,
} from '../../../../shared/constants/gas';
import { decGWEIToHexWEI } from '../../../../shared/modules/conversion.utils';
import {
HARDFORKS,
MAINNET,
@ -107,6 +111,7 @@ export default class TransactionController extends EventEmitter {
this.inProcessOfSigning = new Set();
this._trackMetaMetricsEvent = opts.trackMetaMetricsEvent;
this._getParticipateInMetrics = opts.getParticipateInMetrics;
this._getEIP1559GasFeeEstimates = opts.getEIP1559GasFeeEstimates;
this.memStore = new ObservableStore({});
this.query = new EthQuery(this.provider);
@ -399,7 +404,13 @@ export default class TransactionController extends EventEmitter {
* @returns {Promise<object>} resolves with txMeta
*/
async addTxGasDefaults(txMeta, getCodeResponse) {
const defaultGasPrice = await this._getDefaultGasPrice(txMeta);
const eip1559Compatibility = await this.getEIP1559Compatibility();
const {
gasPrice: defaultGasPrice,
maxFeePerGas: defaultMaxFeePerGas,
maxPriorityFeePerGas: defaultMaxPriorityFeePerGas,
} = await this._getDefaultGasFees(txMeta, eip1559Compatibility);
const {
gasLimit: defaultGasLimit,
simulationFails,
@ -410,6 +421,67 @@ export default class TransactionController extends EventEmitter {
if (simulationFails) {
txMeta.simulationFails = simulationFails;
}
if (eip1559Compatibility) {
// If the dapp has suggested a gas price, but no maxFeePerGas or maxPriorityFeePerGas
// then we set maxFeePerGas and maxPriorityFeePerGas to the suggested gasPrice.
if (
txMeta.txParams.gasPrice &&
!txMeta.txParams.maxFeePerGas &&
!txMeta.txParams.maxPriorityFeePerGas
) {
txMeta.txParams.maxFeePerGas = txMeta.txParams.gasPrice;
txMeta.txParams.maxPriorityFeePerGas = txMeta.txParams.gasPrice;
} else {
if (defaultMaxFeePerGas && !txMeta.txParams.maxFeePerGas) {
// If the dapp has not set the gasPrice or the maxFeePerGas, then we set maxFeePerGas
// with the one returned by the gasFeeController, if that is available.
txMeta.txParams.maxFeePerGas = defaultMaxFeePerGas;
}
if (
defaultMaxPriorityFeePerGas &&
!txMeta.txParams.maxPriorityFeePerGas
) {
// If the dapp has not set the gasPrice or the maxPriorityFeePerGas, then we set maxPriorityFeePerGas
// with the one returned by the gasFeeController, if that is available.
txMeta.txParams.maxPriorityFeePerGas = defaultMaxPriorityFeePerGas;
}
if (defaultGasPrice && !txMeta.txParams.maxFeePerGas) {
// If the dapp has not set the gasPrice or the maxFeePerGas, and no maxFeePerGas is available
// from the gasFeeController, then we set maxFeePerGas to the defaultGasPrice, assuming it is
// available.
txMeta.txParams.maxFeePerGas = defaultGasPrice;
}
if (
txMeta.txParams.maxFeePerGas &&
!txMeta.txParams.maxPriorityFeePerGas
) {
// If the dapp has not set the gasPrice or the maxPriorityFeePerGas, and no maxPriorityFeePerGas is
// available from the gasFeeController, then we set maxPriorityFeePerGas to
// txMeta.txParams.maxFeePerGas, which will either be the gasPrice from the controller, the maxFeePerGas
// set by the dapp, or the maxFeePerGas from the controller.
txMeta.txParams.maxPriorityFeePerGas = txMeta.txParams.maxFeePerGas;
}
}
// We remove the gasPrice param entirely when on an eip1559 compatible network
delete txMeta.txParams.gasPrice;
} else {
// We ensure that maxFeePerGas and maxPriorityFeePerGas are not in the transaction params
// when not on a EIP1559 compatible network
delete txMeta.txParams.maxPriorityFeePerGas;
delete txMeta.txParams.maxFeePerGas;
}
// If we have gotten to this point, and none of gasPrice, maxPriorityFeePerGas or maxFeePerGas are
// set on txParams, it means that either we are on a non-EIP1559 network and the dapp didn't suggest
// a gas price, or we are on an EIP1559 network, and none of gasPrice, maxPriorityFeePerGas or maxFeePerGas
// were available from either the dapp or the network.
if (
defaultGasPrice &&
!txMeta.txParams.gasPrice &&
@ -418,6 +490,7 @@ export default class TransactionController extends EventEmitter {
) {
txMeta.txParams.gasPrice = defaultGasPrice;
}
if (defaultGasLimit && !txMeta.txParams.gas) {
txMeta.txParams.gas = defaultGasLimit;
}
@ -425,20 +498,59 @@ export default class TransactionController extends EventEmitter {
}
/**
* Gets default gas price, or returns `undefined` if gas price is already set
* Gets default gas fees, or returns `undefined` if gas fees are already set
* @param {Object} txMeta - The txMeta object
* @returns {Promise<string|undefined>} The default gas price
*/
async _getDefaultGasPrice(txMeta) {
async _getDefaultGasFees(txMeta, eip1559Compatibility) {
if (
txMeta.txParams.gasPrice ||
(!eip1559Compatibility && txMeta.txParams.gasPrice) ||
(txMeta.txParams.maxFeePerGas && txMeta.txParams.maxPriorityFeePerGas)
) {
return undefined;
return {};
}
try {
const {
gasFeeEstimates,
gasEstimateType,
} = await this._getEIP1559GasFeeEstimates();
if (
eip1559Compatibility &&
gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET
) {
const {
medium: { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = {},
} = gasFeeEstimates;
if (suggestedMaxPriorityFeePerGas && suggestedMaxFeePerGas) {
return {
maxFeePerGas: decGWEIToHexWEI(suggestedMaxFeePerGas),
maxPriorityFeePerGas: decGWEIToHexWEI(
suggestedMaxPriorityFeePerGas,
),
};
}
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
// The LEGACY type includes low, medium and high estimates of
// gas price values.
return {
gasPrice: decGWEIToHexWEI(gasFeeEstimates.medium),
};
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
// The ETH_GASPRICE type just includes a single gas price property,
// which we can assume was retrieved from eth_gasPrice
return {
gasPrice: decGWEIToHexWEI(gasFeeEstimates.gasPrice),
};
}
} catch (e) {
console.error(e);
}
const gasPrice = await this.query.gasPrice();
return addHexPrefix(gasPrice.toString(16));
return { gasPrice: gasPrice && addHexPrefix(gasPrice.toString(16)) };
}
/**
@ -682,6 +794,7 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.setTxStatusApproved(txId);
// get next nonce
const txMeta = this.txStateManager.getTransaction(txId);
const fromAddress = txMeta.txParams.from;
// wait for a nonce
let { customNonceValue } = txMeta;

@ -14,6 +14,7 @@ import {
TRANSACTION_TYPES,
} from '../../../../shared/constants/transaction';
import { SECOND } from '../../../../shared/constants/time';
import { GAS_ESTIMATE_TYPES } from '../../../../shared/constants/gas';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import TransactionController, { TRANSACTION_EVENTS } from '.';
@ -50,9 +51,8 @@ describe('Transaction Controller', function () {
return '0xee6b2800';
},
networkStore: new ObservableStore(currentNetworkId),
getEIP1559Compatibility: () => Promise.resolve(true),
getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(true),
getCurrentAccountEIP1559Compatibility: () => true,
getCurrentNetworkEIP1559Compatibility: () => Promise.resolve(false),
getCurrentAccountEIP1559Compatibility: () => false,
txHistoryLimit: 10,
blockTracker: blockTrackerStub,
signTransaction: (ethTx) =>
@ -64,6 +64,7 @@ describe('Transaction Controller', function () {
getCurrentChainId: () => currentChainId,
getParticipateInMetrics: () => false,
trackMetaMetricsEvent: () => undefined,
getEIP1559GasFeeEstimates: () => undefined,
});
txController.nonceTracker.getNonceLock = () =>
Promise.resolve({ nextNonce: 0, releaseLock: noop });
@ -419,6 +420,237 @@ describe('Transaction Controller', function () {
'should have added the gas field',
);
});
it('should add EIP1559 tx defaults', async function () {
const TEST_MAX_FEE_PER_GAS = '0x12a05f200';
const TEST_MAX_PRIORITY_FEE_PER_GAS = '0x77359400';
const stub1 = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
const stub2 = sinon
.stub(txController, '_getDefaultGasFees')
.callsFake(() => ({
maxFeePerGas: TEST_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: TEST_MAX_PRIORITY_FEE_PER_GAS,
}));
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
const txMeta = {
id: 1,
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
history: [{}],
};
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' };
providerResultStub.eth_estimateGas = '5209';
const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta);
assert.equal(
txMetaWithDefaults.txParams.maxFeePerGas,
TEST_MAX_FEE_PER_GAS,
'should have added the correct max fee per gas',
);
assert.equal(
txMetaWithDefaults.txParams.maxPriorityFeePerGas,
TEST_MAX_PRIORITY_FEE_PER_GAS,
'should have added the correct max priority fee per gas',
);
stub1.restore();
stub2.restore();
});
it('should add gasPrice as maxFeePerGas and maxPriorityFeePerGas if there are no sources of other fee data available', async function () {
const TEST_GASPRICE = '0x12a05f200';
const stub1 = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
const stub2 = sinon
.stub(txController, '_getDefaultGasFees')
.callsFake(() => ({ gasPrice: TEST_GASPRICE }));
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
},
history: [{}],
},
]);
const txMeta = {
id: 1,
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
history: [{}],
};
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' };
providerResultStub.eth_estimateGas = '5209';
const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta);
assert.equal(
txMetaWithDefaults.txParams.maxFeePerGas,
TEST_GASPRICE,
'should have added the correct max fee per gas',
);
assert.equal(
txMetaWithDefaults.txParams.maxPriorityFeePerGas,
TEST_GASPRICE,
'should have added the correct max priority fee per gas',
);
stub1.restore();
stub2.restore();
});
it('should not add gasPrice if the fee data is available from the dapp', async function () {
const TEST_GASPRICE = '0x12a05f200';
const TEST_MAX_FEE_PER_GAS = '0x12a05f200';
const TEST_MAX_PRIORITY_FEE_PER_GAS = '0x77359400';
const stub1 = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
const stub2 = sinon
.stub(txController, '_getDefaultGasFees')
.callsFake(() => ({ gasPrice: TEST_GASPRICE }));
txController.txStateManager._addTransactionsToState([
{
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
metamaskNetworkId: currentNetworkId,
txParams: {
to: VALID_ADDRESS,
from: VALID_ADDRESS_TWO,
maxFeePerGas: TEST_MAX_FEE_PER_GAS,
maxPriorityFeePerGas: TEST_MAX_PRIORITY_FEE_PER_GAS,
},
history: [{}],
},
]);
const txMeta = {
id: 1,
txParams: {
from: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
to: '0xc684832530fcbddae4b4230a47e991ddcec2831d',
},
history: [{}],
};
providerResultStub.eth_getBlockByNumber = { gasLimit: '47b784' };
providerResultStub.eth_estimateGas = '5209';
const txMetaWithDefaults = await txController.addTxGasDefaults(txMeta);
assert.equal(
txMetaWithDefaults.txParams.maxFeePerGas,
TEST_MAX_FEE_PER_GAS,
'should have added the correct max fee per gas',
);
assert.equal(
txMetaWithDefaults.txParams.maxPriorityFeePerGas,
TEST_MAX_PRIORITY_FEE_PER_GAS,
'should have added the correct max priority fee per gas',
);
stub1.restore();
stub2.restore();
});
});
describe('_getDefaultGasFees', function () {
let getGasFeeStub;
beforeEach(function () {
getGasFeeStub = sinon.stub(txController, '_getEIP1559GasFeeEstimates');
});
afterEach(function () {
getGasFeeStub.restore();
});
it('should return the correct fee data when the gas estimate type is FEE_MARKET', async function () {
const EXPECTED_MAX_FEE_PER_GAS = '12a05f200';
const EXPECTED_MAX_PRIORITY_FEE_PER_GAS = '77359400';
getGasFeeStub.callsFake(() => ({
gasFeeEstimates: {
medium: {
suggestedMaxPriorityFeePerGas: '2',
suggestedMaxFeePerGas: '5',
},
},
gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET,
}));
const defaultGasFees = await txController._getDefaultGasFees(
{ txParams: {} },
true,
);
assert.deepEqual(defaultGasFees, {
maxPriorityFeePerGas: EXPECTED_MAX_PRIORITY_FEE_PER_GAS,
maxFeePerGas: EXPECTED_MAX_FEE_PER_GAS,
});
});
it('should return the correct fee data when the gas estimate type is LEGACY', async function () {
const EXPECTED_GAS_PRICE = '77359400';
getGasFeeStub.callsFake(() => ({
gasFeeEstimates: { medium: '2' },
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
}));
const defaultGasFees = await txController._getDefaultGasFees(
{ txParams: {} },
false,
);
assert.deepEqual(defaultGasFees, {
gasPrice: EXPECTED_GAS_PRICE,
});
});
it('should return the correct fee data when the gas estimate type is ETH_GASPRICE', async function () {
const EXPECTED_GAS_PRICE = '77359400';
getGasFeeStub.callsFake(() => ({
gasFeeEstimates: { gasPrice: '2' },
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
}));
const defaultGasFees = await txController._getDefaultGasFees(
{ txParams: {} },
false,
);
assert.deepEqual(defaultGasFees, {
gasPrice: EXPECTED_GAS_PRICE,
});
});
});
describe('#addTransaction', function () {
@ -807,6 +1039,9 @@ describe('Transaction Controller', function () {
});
it('sets txParams.type to 0x2 (EIP-1559)', async function () {
const eip1559CompatibilityStub = sinon
.stub(txController, 'getEIP1559Compatibility')
.returns(true);
txController.txStateManager._addTransactionsToState([
{
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -825,6 +1060,7 @@ describe('Transaction Controller', function () {
]);
await txController.signTransaction('2');
assert.equal(fromTxDataSpy.getCall(0).args[0].type, '0x2');
eip1559CompatibilityStub.restore();
});
});

@ -423,6 +423,9 @@ export default class MetamaskController extends EventEmitter {
),
getParticipateInMetrics: () =>
this.metaMetricsController.state.participateInMetaMetrics,
getEIP1559GasFeeEstimates: this.gasFeeController.fetchGasFeeEstimates.bind(
this.gasFeeController,
),
});
this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation());

@ -1,7 +1,10 @@
module.exports = {
restoreMocks: true,
coverageDirectory: 'jest-coverage/',
collectCoverageFrom: ['<rootDir>/ui/**/swaps/**'],
collectCoverageFrom: [
'<rootDir>/ui/**/swaps/**',
'<rootDir>/ui/ducks/send/**',
],
coveragePathIgnorePatterns: ['.stories.js', '.snap'],
coverageThreshold: {
global: {

@ -268,7 +268,7 @@ const toNegative = (n, options = {}) => {
return multiplyCurrencies(n, -1, options);
};
export function decGWEIToHexWEI(decGWEI) {
function decGWEIToHexWEI(decGWEI) {
return conversionUtil(decGWEI, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
@ -288,4 +288,5 @@ export {
conversionMax,
toNegative,
subtractCurrencies,
decGWEIToHexWEI,
};

@ -34,31 +34,33 @@ export default function AdvancedGasControls({
maxFeeFiat,
gasErrors,
minimumGasLimit = 21000,
networkSupportsEIP1559,
}) {
const t = useContext(I18nContext);
const suggestedValues = {};
switch (gasEstimateType) {
case GAS_ESTIMATE_TYPES.FEE_MARKET:
suggestedValues.maxPriorityFeePerGas =
gasFeeEstimates?.[estimateToUse]?.suggestedMaxPriorityFeePerGas;
suggestedValues.maxFeePerGas =
gasFeeEstimates?.[estimateToUse]?.suggestedMaxFeePerGas;
break;
case GAS_ESTIMATE_TYPES.LEGACY:
suggestedValues.gasPrice = gasFeeEstimates?.[estimateToUse];
break;
case GAS_ESTIMATE_TYPES.ETH_GASPRICE:
suggestedValues.gasPrice = gasFeeEstimates?.gasPrice;
break;
default:
break;
if (networkSupportsEIP1559) {
suggestedValues.maxFeePerGas =
gasFeeEstimates?.[estimateToUse]?.suggestedMaxFeePerGas ||
gasFeeEstimates?.gasPrice;
suggestedValues.maxPriorityFeePerGas =
gasFeeEstimates?.[estimateToUse]?.suggestedMaxPriorityFeePerGas ||
suggestedValues.maxFeePerGas;
} else {
switch (gasEstimateType) {
case GAS_ESTIMATE_TYPES.LEGACY:
suggestedValues.gasPrice = gasFeeEstimates?.[estimateToUse];
break;
case GAS_ESTIMATE_TYPES.ETH_GASPRICE:
suggestedValues.gasPrice = gasFeeEstimates?.gasPrice;
break;
default:
break;
}
}
const showFeeMarketFields =
process.env.SHOW_EIP_1559_UI &&
gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET;
const showFeeMarketFields = networkSupportsEIP1559;
return (
<div className="advanced-gas-controls">
@ -233,4 +235,5 @@ AdvancedGasControls.propTypes = {
maxFeeFiat: PropTypes.string,
gasErrors: PropTypes.object,
minimumGasLimit: PropTypes.number,
networkSupportsEIP1559: PropTypes.object,
};

@ -7,10 +7,10 @@ import {
EDIT_GAS_MODES,
} from '../../../../shared/constants/gas';
import { isEIP1559Network } from '../../../ducks/metamask/metamask';
import Button from '../../ui/button';
import Typography from '../../ui/typography/typography';
import { isEIP1559Network } from '../../../ducks/metamask/metamask';
import {
COLORS,
TYPOGRAPHY,
@ -63,6 +63,7 @@ export default function EditGasDisplay({
balanceError,
}) {
const t = useContext(I18nContext);
const supportsEIP1559 = useSelector(isEIP1559Network);
const dappSuggestedAndTxParamGasFeesAreTheSame = areDappSuggestedAndTxParamGasFeesTheSame(
transaction,
@ -220,6 +221,7 @@ export default function EditGasDisplay({
gasErrors={gasErrors}
onManualChange={onManualChange}
minimumGasLimit={minimumGasLimit}
networkSupportsEIP1559={supportsEIP1559}
/>
)}
</div>

@ -1,13 +1,11 @@
import React, { useCallback, useContext, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { isEIP1559Network } from '../../../ducks/metamask/metamask';
import { useGasFeeInputs } from '../../../hooks/useGasFeeInputs';
import { useShouldAnimateGasEstimations } from '../../../hooks/useShouldAnimateGasEstimations';
import {
GAS_ESTIMATE_TYPES,
EDIT_GAS_MODES,
} from '../../../../shared/constants/gas';
import { EDIT_GAS_MODES } from '../../../../shared/constants/gas';
import {
decGWEIToHexWEI,
@ -44,6 +42,7 @@ export default function EditGasPopover({
const t = useContext(I18nContext);
const dispatch = useDispatch();
const showSidebar = useSelector((state) => state.appState.sidebar.isOpen);
const supportsEIP1559 = useSelector(isEIP1559Network);
const shouldAnimate = useShouldAnimateGasEstimations();
@ -110,19 +109,20 @@ export default function EditGasPopover({
closePopover();
}
const newGasSettings =
gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET
? {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas),
maxPriorityFeePerGas: decGWEIToHexWEI(maxPriorityFeePerGas),
}
: {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
gasPrice: decGWEIToHexWEI(gasPrice),
};
const newGasSettings = supportsEIP1559
? {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
maxFeePerGas: decGWEIToHexWEI(maxFeePerGas ?? gasPrice),
maxPriorityFeePerGas: decGWEIToHexWEI(
maxPriorityFeePerGas ?? maxFeePerGas ?? gasPrice,
),
}
: {
gas: decimalToHex(gasLimit),
gasLimit: decimalToHex(gasLimit),
gasPrice: decGWEIToHexWEI(gasPrice),
};
switch (mode) {
case EDIT_GAS_MODES.CANCEL:
@ -144,7 +144,7 @@ export default function EditGasPopover({
break;
case EDIT_GAS_MODES.SWAPS:
// This popover component should only be used for the "FEE_MARKET" type in Swaps.
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
if (supportsEIP1559) {
dispatch(updateCustomSwapsEIP1559GasParams(newGasSettings));
}
break;
@ -162,7 +162,7 @@ export default function EditGasPopover({
gasPrice,
maxFeePerGas,
maxPriorityFeePerGas,
gasEstimateType,
supportsEIP1559,
]);
let title = t('editGasTitle');

@ -73,6 +73,7 @@ import {
getGasEstimateType,
getTokens,
getUnapprovedTxs,
isEIP1559Network,
} from '../metamask/metamask';
import { resetEnsResolution } from '../ens';
import {
@ -81,6 +82,7 @@ import {
} from '../../../shared/modules/hexstring-utils';
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network';
import { ETH, GWEI } from '../../helpers/constants/common';
import { TRANSACTION_ENVELOPE_TYPES } from '../../../shared/constants/transaction';
// typedefs
/**
@ -91,7 +93,7 @@ const name = 'send';
/**
* The Stages that the send slice can be in
* 1. UNINITIALIZED - The send state is idle, and hasn't yet fetched required
* 1. INACTIVE - The send state is idle, and hasn't yet fetched required
* data for gasPrice and gasLimit estimations, etc.
* 2. ADD_RECIPIENT - The user is selecting which address to send an asset to
* 3. DRAFT - The send form is shown for a transaction yet to be sent to the
@ -407,6 +409,7 @@ export const initializeSendState = createAsyncThunk(
const state = thunkApi.getState();
const isNonStandardEthChain = getIsNonStandardEthChain(state);
const chainId = getCurrentChainId(state);
const eip1559support = isEIP1559Network(state);
const {
send: { asset, stage, recipient, amount, draftTransaction },
metamask,
@ -424,7 +427,10 @@ export const initializeSendState = createAsyncThunk(
// selector and returns the account at the specified address.
const account = getTargetAccount(state, fromAddress);
// Default gasPrice to 1 gwei if all estimation fails
// Default gasPrice to 1 gwei if all estimation fails, this is only used
// for gasLimit estimation and won't be set directly in state. Instead, we
// will return the gasFeeEstimates and gasEstimateType so that the reducer
// can set the appropriate gas fees in state.
let gasPrice = '0x1';
let gasEstimatePollToken = null;
@ -434,6 +440,9 @@ export const initializeSendState = createAsyncThunk(
metamask: { gasFeeEstimates, gasEstimateType },
} = thunkApi.getState();
// Because we are only interested in getting a gasLimit estimation we only
// need to worry about gasPrice. So we use maxFeePerGas as gasPrice if we
// have a fee market estimation.
if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
gasPrice = getGasPriceInHexWei(gasFeeEstimates.medium);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
@ -442,6 +451,10 @@ export const initializeSendState = createAsyncThunk(
gasPrice = getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxFeePerGas,
);
} else {
gasPrice = gasFeeEstimates.gasPrice
? getRoundedGasPrice(gasFeeEstimates.gasPrice)
: '0x0';
}
// Set a basic gasLimit in the event that other estimation fails
@ -493,19 +506,25 @@ export const initializeSendState = createAsyncThunk(
assetBalance: balance,
chainId: getCurrentChainId(state),
tokens: getTokens(state),
gasPrice,
gasFeeEstimates,
gasEstimateType,
gasLimit,
gasTotal: addHexPrefix(calcGasTotal(gasLimit, gasPrice)),
gasEstimatePollToken,
eip1559support,
};
},
);
export const initialState = {
// which stage of the send flow is the user on
stage: SEND_STAGES.UNINITIALIZED,
stage: SEND_STAGES.INACTIVE,
// status of the send slice, either VALID or INVALID
status: SEND_STATUSES.VALID,
// Determines type of transaction being sent, defaulted to 0x0 (legacy)
transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
// tracks whether the current network supports EIP 1559 transactions
eip1559support: false,
account: {
// from account address, defaults to selected account. will be the account
// the original transaction was sent from in the case of the EDIT stage
@ -524,6 +543,10 @@ export const initialState = {
gasLimit: '0x0',
// price in wei to pay per gas
gasPrice: '0x0',
// maximum price in wei to pay per gas
maxFeePerGas: '0x0',
// maximum priority fee in wei to pay per gas
maxPriorityFeePerGas: '0x0',
// expected price in wei necessary to pay per gas used for a transaction
// to be included in a reasonable timeframe. Comes from GasFeeController.
gasPriceEstimate: '0x0',
@ -570,6 +593,7 @@ export const initialState = {
value: '0x0',
gas: '0x0',
gasPrice: '0x0',
type: TRANSACTION_ENVELOPE_TYPES.LEGACY,
},
},
recipient: {
@ -683,9 +707,17 @@ const slice = createSlice({
* field and send state, then updates the draft transaction.
*/
calculateGasTotal: (state) => {
state.gas.gasTotal = addHexPrefix(
calcGasTotal(state.gas.gasLimit, state.gas.gasPrice),
);
// use maxFeePerGas as the multiplier if working with a FEE_MARKET transaction
// otherwise use gasPrice
if (state.transactionType === TRANSACTION_ENVELOPE_TYPES.FEE_MARKET) {
state.gas.gasTotal = addHexPrefix(
calcGasTotal(state.gas.gasLimit, state.gas.maxFeePerGas),
);
} else {
state.gas.gasTotal = addHexPrefix(
calcGasTotal(state.gas.gasLimit, state.gas.gasPrice),
);
}
if (
state.amount.mode === AMOUNT_MODES.MAX &&
state.asset.type === ASSET_TYPES.NATIVE
@ -705,12 +737,86 @@ const slice = createSlice({
slice.caseReducers.calculateGasTotal(state);
},
/**
* sets the provided gasPrice in state and then recomputes the gasTotal
* Sets the appropriate gas fees in state and determines and sets the
* appropriate transactionType based on gas fee fields received.
*/
updateGasPrice: (state, action) => {
state.gas.gasPrice = addHexPrefix(action.payload);
updateGasFees: (state, action) => {
if (
action.payload.transactionType === TRANSACTION_ENVELOPE_TYPES.FEE_MARKET
) {
state.gas.maxFeePerGas = addHexPrefix(action.payload.maxFeePerGas);
state.gas.maxPriorityFeePerGas = addHexPrefix(
action.payload.maxPriorityFeePerGas,
);
state.transactionType = TRANSACTION_ENVELOPE_TYPES.FEE_MARKET;
} else {
// Until we remove the old UI we don't want to automatically update
// gasPrice if the user has already manually changed the field value.
// When receiving a new estimate the isAutomaticUpdate property will be
// on the payload (and set to true). If isAutomaticUpdate is true,
// then we check if the previous estimate was '0x0' or if the previous
// gasPrice equals the previous gasEstimate. if either of those cases
// are true then we update the gasPrice otherwise we skip it because
// it indicates the user has ejected from the estimates by modifying
// the field.
if (
action.payload.isAutomaticUpdate !== true ||
state.gas.gasPriceEstimate === '0x0' ||
state.gas.gasPrice === state.gas.gasPriceEstimate
) {
state.gas.gasPrice = addHexPrefix(action.payload.gasPrice);
}
state.transactionType = TRANSACTION_ENVELOPE_TYPES.LEGACY;
}
slice.caseReducers.calculateGasTotal(state);
},
/**
* Sets the appropriate gas fees in state after receiving new estimates.
*/
updateGasFeeEstimates: (state, action) => {
const { gasFeeEstimates, gasEstimateType } = action.payload;
let gasPriceEstimate = '0x0';
switch (gasEstimateType) {
case GAS_ESTIMATE_TYPES.FEE_MARKET:
slice.caseReducers.updateGasFees(state, {
payload: {
transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET,
maxFeePerGas: getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxFeePerGas,
),
maxPriorityFeePerGas: getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxPriorityFeePerGas,
),
},
});
break;
case GAS_ESTIMATE_TYPES.LEGACY:
gasPriceEstimate = getRoundedGasPrice(gasFeeEstimates.medium);
slice.caseReducers.updateGasFees(state, {
payload: {
gasPrice: gasPriceEstimate,
type: TRANSACTION_ENVELOPE_TYPES.LEGACY,
isAutomaticUpdate: true,
},
});
break;
case GAS_ESTIMATE_TYPES.ETH_GASPRICE:
gasPriceEstimate = getRoundedGasPrice(gasFeeEstimates.gasPrice);
slice.caseReducers.updateGasFees(state, {
payload: {
gasPrice: getRoundedGasPrice(gasFeeEstimates.gasPrice),
type: TRANSACTION_ENVELOPE_TYPES.LEGACY,
isAutomaticUpdate: true,
},
});
break;
case GAS_ESTIMATE_TYPES.NONE:
default:
break;
}
// Record the latest gasPriceEstimate for future comparisons
state.gas.gasPriceEstimate = addHexPrefix(gasPriceEstimate);
},
/**
* sets the amount mode to the provided value as long as it is one of the
* supported modes (MAX|INPUT)
@ -778,9 +884,15 @@ const slice = createSlice({
// We keep a copy of txParams in state that could be submitted to the
// network if the form state is valid.
if (state.status === SEND_STATUSES.VALID) {
// We don't/shouldn't modify the from address when editing an
// existing transaction.
if (state.stage !== SEND_STAGES.EDIT) {
state.draftTransaction.txParams.from = state.account.address;
}
// gasLimit always needs to be set regardless of the asset being sent
// or the type of transaction.
state.draftTransaction.txParams.gas = state.gas.gasLimit;
switch (state.asset.type) {
case ASSET_TYPES.TOKEN:
// When sending a token the to address is the contract address of
@ -789,8 +901,6 @@ const slice = createSlice({
// amount.
state.draftTransaction.txParams.to = state.asset.details.address;
state.draftTransaction.txParams.value = '0x0';
state.draftTransaction.txParams.gas = state.gas.gasLimit;
state.draftTransaction.txParams.gasPrice = state.gas.gasPrice;
state.draftTransaction.txParams.data = generateTokenTransferData({
toAddress: state.recipient.address,
amount: state.amount.value,
@ -804,11 +914,46 @@ const slice = createSlice({
// populated with the user input provided in hex field.
state.draftTransaction.txParams.to = state.recipient.address;
state.draftTransaction.txParams.value = state.amount.value;
state.draftTransaction.txParams.gas = state.gas.gasLimit;
state.draftTransaction.txParams.gasPrice = state.gas.gasPrice;
state.draftTransaction.txParams.data =
state.draftTransaction.userInputHexData ?? undefined;
}
// We need to make sure that we only include the right gas fee fields
// based on the type of transaction the network supports. We will also set
// the type param here. We must delete the opposite fields to avoid
// stale data in txParams.
if (state.eip1559support) {
state.draftTransaction.txParams.type =
TRANSACTION_ENVELOPE_TYPES.FEE_MARKET;
state.draftTransaction.txParams.maxFeePerGas = state.gas.maxFeePerGas;
state.draftTransaction.txParams.maxPriorityFeePerGas =
state.gas.maxPriorityFeePerGas;
if (
!state.draftTransaction.txParams.maxFeePerGas ||
state.draftTransaction.txParams.maxFeePerGas === '0x0'
) {
state.draftTransaction.txParams.maxFeePerGas = state.gas.gasPrice;
}
if (
!state.draftTransaction.txParams.maxPriorityFeePerGas ||
state.draftTransaction.txParams.maxPriorityFeePerGas === '0x0'
) {
state.draftTransaction.txParams.maxPriorityFeePerGas =
state.draftTransaction.txParams.maxFeePerGas;
}
delete state.draftTransaction.txParams.gasPrice;
} else {
delete state.draftTransaction.txParams.maxFeePerGas;
delete state.draftTransaction.txParams.maxPriorityFeePerGas;
state.draftTransaction.txParams.gasPrice = state.gas.gasPrice;
state.draftTransaction.txParams.type =
TRANSACTION_ENVELOPE_TYPES.LEGACY;
}
}
},
useDefaultGas: (state) => {
@ -937,7 +1082,7 @@ const slice = createSlice({
case state.asset.type === ASSET_TYPES.TOKEN &&
state.asset.details === null:
case state.stage === SEND_STAGES.ADD_RECIPIENT:
case state.stage === SEND_STAGES.UNINITIALIZED:
case state.stage === SEND_STAGES.INACTIVE:
case state.gas.isGasEstimateLoading:
case new BigNumber(state.gas.gasLimit, 16).lessThan(
new BigNumber(state.gas.minimumGasLimit),
@ -1039,17 +1184,23 @@ const slice = createSlice({
.addCase(initializeSendState.fulfilled, (state, action) => {
// writes the computed initialized state values into the slice and then
// calculates slice validity using the caseReducers.
state.eip1559support = action.payload.eip1559support;
state.account.address = action.payload.address;
state.account.balance = action.payload.nativeBalance;
state.asset.balance = action.payload.assetBalance;
state.gas.gasLimit = action.payload.gasLimit;
state.gas.gasPrice = action.payload.gasPrice;
slice.caseReducers.updateGasFeeEstimates(state, {
payload: {
gasFeeEstimates: action.payload.gasFeeEstimates,
gasEstimateType: action.payload.gasEstimateType,
},
});
state.gas.gasTotal = action.payload.gasTotal;
state.gas.gasEstimatePollToken = action.payload.gasEstimatePollToken;
if (action.payload.gasEstimatePollToken) {
state.gas.isGasEstimateLoading = false;
}
if (state.stage !== SEND_STAGES.UNINITIALIZED) {
if (state.stage !== SEND_STAGES.INACTIVE) {
slice.caseReducers.validateRecipientUserInput(state, {
payload: {
chainId: action.payload.chainId,
@ -1058,7 +1209,7 @@ const slice = createSlice({
});
}
state.stage =
state.stage === SEND_STAGES.UNINITIALIZED
state.stage === SEND_STAGES.INACTIVE
? SEND_STAGES.ADD_RECIPIENT
: state.stage;
slice.caseReducers.validateAmountField(state);
@ -1091,33 +1242,9 @@ const slice = createSlice({
.addCase(GAS_FEE_ESTIMATES_UPDATED, (state, action) => {
// When the gasFeeController updates its gas fee estimates we need to
// update and validate state based on those new values
const { gasFeeEstimates, gasEstimateType } = action.payload;
let payload = null;
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
payload = getGasPriceInHexWei(
gasFeeEstimates.medium.suggestedMaxFeePerGas,
);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) {
payload = getGasPriceInHexWei(gasFeeEstimates.medium);
} else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) {
payload = getRoundedGasPrice(gasFeeEstimates.gasPrice);
}
// If a new gasPrice can be derived, and either the gasPriceEstimate
// was '0x0' or the gasPrice selected matches the previous estimate,
// update the gasPrice. This will ensure that we only update the
// gasPrice if the user is using our previous estimated value.
if (
payload &&
(state.gas.gasPriceEstimate === '0x0' ||
state.gas.gasPrice === state.gas.gasPriceEstimate)
) {
slice.caseReducers.updateGasPrice(state, {
payload,
});
}
// Record the latest gasPriceEstimate for future comparisons
state.gas.gasPriceEstimate = payload ?? state.gas.gasPriceEstimate;
slice.caseReducers.updateGasFeeEstimates(state, {
payload: action.payload,
});
});
},
});
@ -1130,15 +1257,37 @@ const {
useDefaultGas,
useCustomGas,
updateGasLimit,
updateGasPrice,
validateRecipientUserInput,
updateRecipientSearchMode,
} = actions;
export { useDefaultGas, useCustomGas, updateGasLimit, updateGasPrice };
export { useDefaultGas, useCustomGas, updateGasLimit };
// Action Creators
/**
* This method is a temporary placeholder to support the old UI in both the
* gas modal and the send flow. Soon we won't need to modify gasPrice from the
* send flow based on user input, it'll just be a shallow copy of the current
* estimate. This method is necessary because the internal structure of this
* slice has been changed such that it is agnostic to transaction envelope
* type, and this method calls into the new structure in the appropriate way.
*
* @deprecated - don't extend the usage of this temporary method
* @param {string} gasPrice - new gas price in hex wei
* @returns {void}
*/
export function updateGasPrice(gasPrice) {
return (dispatch) => {
dispatch(
actions.updateGasFees({
gasPrice,
transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
}),
);
};
}
export function resetSendState() {
return async (dispatch, getState) => {
const state = getState();
@ -1587,7 +1736,7 @@ export function getSendErrors(state) {
}
export function isSendStateInitialized(state) {
return state[name].stage !== SEND_STAGES.UNINITIALIZED;
return state[name].stage !== SEND_STAGES.INACTIVE;
}
export function isSendFormInvalid(state) {

@ -10,9 +10,15 @@ import {
KNOWN_RECIPIENT_ADDRESS_WARNING,
NEGATIVE_ETH_ERROR,
} from '../../pages/send/send.constants';
import { RINKEBY_CHAIN_ID } from '../../../shared/constants/network';
import {
MAINNET_CHAIN_ID,
RINKEBY_CHAIN_ID,
} from '../../../shared/constants/network';
import { GAS_ESTIMATE_TYPES, GAS_LIMITS } from '../../../shared/constants/gas';
import { TRANSACTION_TYPES } from '../../../shared/constants/transaction';
import {
TRANSACTION_ENVELOPE_TYPES,
TRANSACTION_TYPES,
} from '../../../shared/constants/transaction';
import sendReducer, {
initialState,
initializeSendState,
@ -32,6 +38,30 @@ import sendReducer, {
AMOUNT_MODES,
RECIPIENT_SEARCH_MODES,
editTransaction,
getGasLimit,
getGasPrice,
getGasTotal,
gasFeeIsInError,
getMinimumGasLimitForSend,
getGasInputMode,
GAS_INPUT_MODES,
getSendAsset,
getSendAssetAddress,
getIsAssetSendable,
getSendAmount,
getIsBalanceInsufficient,
getSendMaxModeState,
sendAmountIsInError,
getSendHexData,
getSendTo,
getIsUsingMyAccountForRecipientSearch,
getRecipientUserInput,
getRecipient,
getSendErrors,
isSendStateInitialized,
isSendFormInvalid,
getSendStage,
updateGasPrice,
} from './send';
const mockStore = createMockStore([thunk]);
@ -89,8 +119,48 @@ describe('Send Slice', () => {
});
});
describe('updateGasFees', () => {
it('should work with FEE_MARKET gas fees', () => {
const action = {
type: 'send/updateGasFees',
payload: {
transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET,
maxFeePerGas: '0x2',
maxPriorityFeePerGas: '0x1',
},
};
const result = sendReducer(initialState, action);
expect(result.gas.maxFeePerGas).toStrictEqual(
action.payload.maxFeePerGas,
);
expect(result.gas.maxPriorityFeePerGas).toStrictEqual(
action.payload.maxPriorityFeePerGas,
);
expect(result.transactionType).toBe(
TRANSACTION_ENVELOPE_TYPES.FEE_MARKET,
);
});
it('should work with LEGACY gas fees', () => {
const action = {
type: 'send/updateGasFees',
payload: {
transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
gasPrice: '0x1',
},
};
const result = sendReducer(initialState, action);
expect(result.gas.gasPrice).toStrictEqual(action.payload.gasPrice);
expect(result.transactionType).toBe(TRANSACTION_ENVELOPE_TYPES.LEGACY);
});
});
describe('updateUserInputHexData', () => {
it('should', () => {
it('should update the state with the provided data', () => {
const action = {
type: 'send/updateUserInputHexData',
payload: 'TestData',
@ -142,56 +212,6 @@ describe('Send Slice', () => {
});
});
describe('updateGasPrice', () => {
const action = {
type: 'send/updateGasPrice',
payload: '0x3b9aca00', // 1000000000
};
it('should update gas price and update draft transaction with validated state', () => {
const validSendState = {
...initialState,
stage: SEND_STAGES.DRAFT,
account: {
balance: '0x56bc75e2d63100000',
},
asset: {
balance: '0x56bc75e2d63100000',
type: ASSET_TYPES.NATIVE,
},
gas: {
isGasEstimateLoading: false,
gasTotal: '0x1319718a5000', // 21000000000000
gasLimit: '0x5208', // 21000
minimumGasLimit: '0x5208',
},
};
const result = sendReducer(validSendState, action);
expect(result.gas.gasPrice).toStrictEqual(action.payload);
expect(result.draftTransaction.txParams.gasPrice).toStrictEqual(
action.payload,
);
});
it('should recalculate gasTotal', () => {
const gasState = {
gas: {
gasLimit: '0x5208', // 21000,
gasPrice: '0x0',
},
};
const state = { ...initialState, ...gasState };
const result = sendReducer(state, action);
expect(result.gas.gasPrice).toStrictEqual(action.payload);
expect(result.gas.gasLimit).toStrictEqual(gasState.gas.gasLimit);
expect(result.gas.gasTotal).toStrictEqual('0x1319718a5000'); // 21000000000000
});
});
describe('updateAmountMode', () => {
it('should change to INPUT amount mode', () => {
const emptyAmountModeState = {
@ -342,89 +362,199 @@ describe('Send Slice', () => {
});
describe('updateDraftTransaction', () => {
it('should', () => {
const detailsForDraftTransactionState = {
...initialState,
status: SEND_STATUSES.VALID,
account: {
address: '0xCurrentAddress',
},
asset: {
type: '',
},
recipient: {
address: '0xRecipientAddress',
},
amount: {
value: '0x1',
},
gas: {
gasPrice: '0x3b9aca00', // 1000000000
gasLimit: '0x5208', // 21000
},
};
const action = {
type: 'send/updateDraftTransaction',
};
const result = sendReducer(detailsForDraftTransactionState, action);
describe('with LEGACY transactions', () => {
it('should properly set fields', () => {
const detailsForDraftTransactionState = {
...initialState,
status: SEND_STATUSES.VALID,
transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
account: {
address: '0xCurrentAddress',
},
asset: {
type: '',
},
recipient: {
address: '0xRecipientAddress',
},
amount: {
value: '0x1',
},
gas: {
gasPrice: '0x3b9aca00', // 1000000000
gasLimit: '0x5208', // 21000
},
};
const action = {
type: 'send/updateDraftTransaction',
};
const result = sendReducer(detailsForDraftTransactionState, action);
expect(result.draftTransaction.txParams.to).toStrictEqual(
detailsForDraftTransactionState.recipient.address,
);
expect(result.draftTransaction.txParams.value).toStrictEqual(
detailsForDraftTransactionState.amount.value,
);
expect(result.draftTransaction.txParams.gas).toStrictEqual(
detailsForDraftTransactionState.gas.gasLimit,
);
expect(result.draftTransaction.txParams.gasPrice).toStrictEqual(
detailsForDraftTransactionState.gas.gasPrice,
);
});
expect(result.draftTransaction.txParams.to).toStrictEqual(
detailsForDraftTransactionState.recipient.address,
);
expect(result.draftTransaction.txParams.value).toStrictEqual(
detailsForDraftTransactionState.amount.value,
);
expect(result.draftTransaction.txParams.gas).toStrictEqual(
detailsForDraftTransactionState.gas.gasLimit,
);
expect(result.draftTransaction.txParams.gasPrice).toStrictEqual(
detailsForDraftTransactionState.gas.gasPrice,
);
it('should update the draftTransaction txParams recipient to token address when asset is type TOKEN', () => {
const detailsForDraftTransactionState = {
...initialState,
status: SEND_STATUSES.VALID,
transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
account: {
address: '0xCurrentAddress',
},
asset: {
type: ASSET_TYPES.TOKEN,
details: {
address: '0xTokenAddress',
},
},
amount: {
value: '0x1',
},
gas: {
gasPrice: '0x3b9aca00', // 1000000000
gasLimit: '0x5208', // 21000
},
};
const action = {
type: 'send/updateDraftTransaction',
};
const result = sendReducer(detailsForDraftTransactionState, action);
expect(result.draftTransaction.txParams.to).toStrictEqual(
detailsForDraftTransactionState.asset.details.address,
);
expect(result.draftTransaction.txParams.value).toStrictEqual('0x0');
expect(result.draftTransaction.txParams.gas).toStrictEqual(
detailsForDraftTransactionState.gas.gasLimit,
);
expect(result.draftTransaction.txParams.gasPrice).toStrictEqual(
detailsForDraftTransactionState.gas.gasPrice,
);
expect(result.draftTransaction.txParams.data).toStrictEqual(
'0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',
);
});
});
it('should update the draftTransaction txParams recipient to token address when asset is type TOKEN', () => {
const detailsForDraftTransactionState = {
...initialState,
status: SEND_STATUSES.VALID,
account: {
address: '0xCurrentAddress',
},
asset: {
type: ASSET_TYPES.TOKEN,
details: {
address: '0xTokenAddress',
describe('with FEE_MARKET transactions', () => {
it('should properly set fields', () => {
const detailsForDraftTransactionState = {
...initialState,
status: SEND_STATUSES.VALID,
transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET,
account: {
address: '0xCurrentAddress',
},
},
amount: {
value: '0x1',
},
gas: {
gasPrice: '0x3b9aca00', // 1000000000
gasLimit: '0x5208', // 21000
},
};
const action = {
type: 'send/updateDraftTransaction',
};
const result = sendReducer(detailsForDraftTransactionState, action);
asset: {
type: '',
},
recipient: {
address: '0xRecipientAddress',
},
amount: {
value: '0x1',
},
gas: {
maxFeePerGas: '0x2540be400', // 10 GWEI
maxPriorityFeePerGas: '0x3b9aca00', // 1 GWEI
gasLimit: '0x5208', // 21000
},
eip1559support: true,
};
const action = {
type: 'send/updateDraftTransaction',
};
const result = sendReducer(detailsForDraftTransactionState, action);
expect(result.draftTransaction.txParams.to).toStrictEqual(
detailsForDraftTransactionState.recipient.address,
);
expect(result.draftTransaction.txParams.value).toStrictEqual(
detailsForDraftTransactionState.amount.value,
);
expect(result.draftTransaction.txParams.gas).toStrictEqual(
detailsForDraftTransactionState.gas.gasLimit,
);
expect(result.draftTransaction.txParams.gasPrice).toBeUndefined();
expect(result.draftTransaction.txParams.maxFeePerGas).toStrictEqual(
detailsForDraftTransactionState.gas.maxFeePerGas,
);
expect(
result.draftTransaction.txParams.maxPriorityFeePerGas,
).toStrictEqual(
detailsForDraftTransactionState.gas.maxPriorityFeePerGas,
);
});
expect(result.draftTransaction.txParams.to).toStrictEqual(
detailsForDraftTransactionState.asset.details.address,
);
expect(result.draftTransaction.txParams.value).toStrictEqual('0x0');
expect(result.draftTransaction.txParams.gas).toStrictEqual(
detailsForDraftTransactionState.gas.gasLimit,
);
expect(result.draftTransaction.txParams.gasPrice).toStrictEqual(
detailsForDraftTransactionState.gas.gasPrice,
);
expect(result.draftTransaction.txParams.data).toStrictEqual(
'0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',
);
it('should update the draftTransaction txParams recipient to token address when asset is type TOKEN', () => {
const detailsForDraftTransactionState = {
...initialState,
status: SEND_STATUSES.VALID,
transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET,
account: {
address: '0xCurrentAddress',
},
asset: {
type: ASSET_TYPES.TOKEN,
details: {
address: '0xTokenAddress',
},
},
amount: {
value: '0x1',
},
gas: {
maxFeePerGas: '0x2540be400', // 10 GWEI
maxPriorityFeePerGas: '0x3b9aca00', // 1 GWEI
gasLimit: '0x5208', // 21000
},
eip1559support: true,
};
const action = {
type: 'send/updateDraftTransaction',
};
const result = sendReducer(detailsForDraftTransactionState, action);
expect(result.draftTransaction.txParams.to).toStrictEqual(
detailsForDraftTransactionState.asset.details.address,
);
expect(result.draftTransaction.txParams.value).toStrictEqual('0x0');
expect(result.draftTransaction.txParams.gas).toStrictEqual(
detailsForDraftTransactionState.gas.gasLimit,
);
expect(result.draftTransaction.txParams.maxFeePerGas).toStrictEqual(
detailsForDraftTransactionState.gas.maxFeePerGas,
);
expect(
result.draftTransaction.txParams.maxPriorityFeePerGas,
).toStrictEqual(
detailsForDraftTransactionState.gas.maxPriorityFeePerGas,
);
expect(result.draftTransaction.txParams.gasPrice).toBeUndefined();
expect(result.draftTransaction.txParams.data).toStrictEqual(
'0xa9059cbb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',
);
});
});
});
@ -1032,6 +1162,36 @@ describe('Send Slice', () => {
});
describe('Action Creators', () => {
describe('updateGasPrice', () => {
it('should update gas price and update draft transaction with validated state', async () => {
const store = mockStore({
send: {
gas: {
gasPrice: undefined,
},
},
});
const newGasPrice = '0x0';
await store.dispatch(updateGasPrice(newGasPrice));
const actionResult = store.getActions();
const expectedActionResult = [
{
type: 'send/updateGasFees',
payload: {
gasPrice: '0x0',
transactionType: TRANSACTION_ENVELOPE_TYPES.LEGACY,
},
},
];
expect(actionResult).toStrictEqual(expectedActionResult);
});
});
describe('UpdateSendAmount', () => {
const defaultSendAmountState = {
send: {
@ -2013,4 +2173,329 @@ describe('Send Slice', () => {
});
});
});
describe('selectors', () => {
describe('gas selectors', () => {
it('has a selector that gets gasLimit', () => {
expect(getGasLimit({ send: initialState })).toBe('0x0');
});
it('has a selector that gets gasPrice', () => {
expect(getGasPrice({ send: initialState })).toBe('0x0');
});
it('has a selector that gets gasTotal', () => {
expect(getGasTotal({ send: initialState })).toBe('0x0');
});
it('has a selector to determine if gas fee is in error', () => {
expect(gasFeeIsInError({ send: initialState })).toBe(false);
expect(
gasFeeIsInError({
send: {
...initialState,
gas: {
...initialState.gas,
error: 'yes',
},
},
}),
).toBe(true);
});
it('has a selector that gets minimumGasLimit', () => {
expect(getMinimumGasLimitForSend({ send: initialState })).toBe(
GAS_LIMITS.SIMPLE,
);
});
describe('getGasInputMode selector', () => {
it('returns BASIC when on mainnet and advanced inline gas is false', () => {
expect(
getGasInputMode({
metamask: {
provider: { chainId: MAINNET_CHAIN_ID },
featureFlags: { advancedInlineGas: false },
},
send: initialState,
}),
).toBe(GAS_INPUT_MODES.BASIC);
});
it('returns BASIC when on localhost and advanced inline gas is false and IN_TEST is set', () => {
process.env.IN_TEST = true;
expect(
getGasInputMode({
metamask: {
provider: { chainId: '0x539' },
featureFlags: { advancedInlineGas: false },
},
send: initialState,
}),
).toBe(GAS_INPUT_MODES.BASIC);
process.env.IN_TEST = false;
});
it('returns INLINE when on mainnet and advanced inline gas is true', () => {
expect(
getGasInputMode({
metamask: {
provider: { chainId: MAINNET_CHAIN_ID },
featureFlags: { advancedInlineGas: true },
},
send: initialState,
}),
).toBe(GAS_INPUT_MODES.INLINE);
});
it('returns INLINE when on mainnet and advanced inline gas is false but eth_gasPrice estimate is used', () => {
expect(
getGasInputMode({
metamask: {
provider: { chainId: MAINNET_CHAIN_ID },
featureFlags: { advancedInlineGas: false },
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
},
send: initialState,
}),
).toBe(GAS_INPUT_MODES.INLINE);
});
it('returns INLINE when on mainnet and advanced inline gas is false but eth_gasPrice estimate is used even IN_TEST', () => {
process.env.IN_TEST = true;
expect(
getGasInputMode({
metamask: {
provider: { chainId: MAINNET_CHAIN_ID },
featureFlags: { advancedInlineGas: false },
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
},
send: initialState,
}),
).toBe(GAS_INPUT_MODES.INLINE);
process.env.IN_TEST = false;
});
it('returns CUSTOM if isCustomGasSet is true', () => {
expect(
getGasInputMode({
metamask: {
provider: { chainId: MAINNET_CHAIN_ID },
featureFlags: { advancedInlineGas: true },
},
send: {
...initialState,
gas: {
...initialState.send,
isCustomGasSet: true,
},
},
}),
).toBe(GAS_INPUT_MODES.CUSTOM);
});
});
});
describe('asset selectors', () => {
it('has a selector to get the asset', () => {
expect(getSendAsset({ send: initialState })).toMatchObject(
initialState.asset,
);
});
it('has a selector to get the asset address', () => {
expect(
getSendAssetAddress({
send: {
...initialState,
asset: {
balance: '0x0',
details: { address: '0x0' },
type: ASSET_TYPES.TOKEN,
},
},
}),
).toBe('0x0');
});
it('has a selector that determines if asset is sendable based on ERC721 status', () => {
expect(getIsAssetSendable({ send: initialState })).toBe(true);
expect(
getIsAssetSendable({
send: {
...initialState,
asset: {
...initialState,
type: ASSET_TYPES.TOKEN,
details: { isERC721: true },
},
},
}),
).toBe(false);
});
});
describe('amount selectors', () => {
it('has a selector to get send amount', () => {
expect(getSendAmount({ send: initialState })).toBe('0x0');
});
it('has a selector to get if there is an insufficient funds error', () => {
expect(getIsBalanceInsufficient({ send: initialState })).toBe(false);
expect(
getIsBalanceInsufficient({
send: {
...initialState,
gas: { ...initialState.gas, error: INSUFFICIENT_FUNDS_ERROR },
},
}),
).toBe(true);
});
it('has a selector to get max mode state', () => {
expect(getSendMaxModeState({ send: initialState })).toBe(false);
expect(
getSendMaxModeState({
send: {
...initialState,
amount: { ...initialState.amount, mode: AMOUNT_MODES.MAX },
},
}),
).toBe(true);
});
it('has a selector to get the user entered hex data', () => {
expect(getSendHexData({ send: initialState })).toBeNull();
expect(
getSendHexData({
send: {
...initialState,
draftTransaction: {
...initialState.draftTransaction,
userInputHexData: '0x0',
},
},
}),
).toBe('0x0');
});
it('has a selector to get if there is an amount error', () => {
expect(sendAmountIsInError({ send: initialState })).toBe(false);
expect(
sendAmountIsInError({
send: {
...initialState,
amount: { ...initialState.amount, error: 'any' },
},
}),
).toBe(true);
});
});
describe('recipient selectors', () => {
it('has a selector to get recipient address', () => {
expect(getSendTo({ send: initialState })).toBe('');
expect(
getSendTo({
send: {
...initialState,
recipient: { ...initialState.recipient, address: '0xb' },
},
}),
).toBe('0xb');
});
it('has a selector to check if using the my accounts option for recipient selection', () => {
expect(
getIsUsingMyAccountForRecipientSearch({ send: initialState }),
).toBe(false);
expect(
getIsUsingMyAccountForRecipientSearch({
send: {
...initialState,
recipient: {
...initialState.recipient,
mode: RECIPIENT_SEARCH_MODES.MY_ACCOUNTS,
},
},
}),
).toBe(true);
});
it('has a selector to get recipient user input in input field', () => {
expect(getRecipientUserInput({ send: initialState })).toBe('');
expect(
getRecipientUserInput({
send: {
...initialState,
recipient: {
...initialState.recipient,
userInput: 'domain.eth',
},
},
}),
).toBe('domain.eth');
});
it('has a selector to get recipient state', () => {
expect(getRecipient({ send: initialState })).toMatchObject(
initialState.recipient,
);
});
});
describe('send validity selectors', () => {
it('has a selector to get send errors', () => {
expect(getSendErrors({ send: initialState })).toMatchObject({
gasFee: null,
amount: null,
});
expect(
getSendErrors({
send: {
...initialState,
gas: {
...initialState.gas,
error: 'gasFeeTest',
},
amount: {
...initialState.amount,
error: 'amountTest',
},
},
}),
).toMatchObject({ gasFee: 'gasFeeTest', amount: 'amountTest' });
});
it('has a selector to get send state initialization status', () => {
expect(isSendStateInitialized({ send: initialState })).toBe(false);
expect(
isSendStateInitialized({
send: {
...initialState,
stage: SEND_STATUSES.ADD_RECIPIENT,
},
}),
).toBe(true);
});
it('has a selector to get send state validity', () => {
expect(isSendFormInvalid({ send: initialState })).toBe(false);
expect(
isSendFormInvalid({
send: { ...initialState, status: SEND_STATUSES.INVALID },
}),
).toBe(true);
});
it('has a selector to get send stage', () => {
expect(getSendStage({ send: initialState })).toBe(SEND_STAGES.INACTIVE);
expect(
getSendStage({
send: { ...initialState, stage: SEND_STAGES.ADD_RECIPIENT },
}),
).toBe(SEND_STAGES.ADD_RECIPIENT);
});
});
});
});

@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas';
import {
@ -7,10 +6,7 @@ import {
getGasFeeEstimates,
isEIP1559Network,
} from '../ducks/metamask/metamask';
import {
disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling,
} from '../store/actions';
import { useSafeGasEstimatePolling } from './useSafeGasEstimatePolling';
/**
* @typedef {keyof typeof GAS_ESTIMATE_TYPES} GasEstimateTypes
@ -43,31 +39,17 @@ export function useGasFeeEstimates() {
const gasEstimateType = useSelector(getGasEstimateType);
const gasFeeEstimates = useSelector(getGasFeeEstimates);
const estimatedGasFeeTimeBounds = useSelector(getEstimatedGasFeeTimeBounds);
useEffect(() => {
let active = true;
let pollToken;
getGasFeeEstimatesAndStartPolling().then((newPollToken) => {
if (active) {
pollToken = newPollToken;
} else {
disconnectGasFeeEstimatePoller(newPollToken);
}
});
return () => {
active = false;
if (pollToken) {
disconnectGasFeeEstimatePoller(pollToken);
}
};
}, []);
useSafeGasEstimatePolling();
// We consider the gas estimate to be loading if the gasEstimateType is
// 'NONE' or if the current gasEstimateType does not match the type we expect
// for the current network. e.g, a ETH_GASPRICE estimate when on a network
// supporting EIP-1559.
// 'NONE' or if the current gasEstimateType cannot be supported by the current
// network
const isEIP1559TolerableEstimateType =
gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET ||
gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE;
const isGasEstimatesLoading =
gasEstimateType === GAS_ESTIMATE_TYPES.NONE ||
(supportsEIP1559 && gasEstimateType !== GAS_ESTIMATE_TYPES.FEE_MARKET) ||
(supportsEIP1559 && !isEIP1559TolerableEstimateType) ||
(!supportsEIP1559 && gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET);
return {

@ -173,11 +173,11 @@ describe('useGasFeeEstimates', () => {
});
});
it('indicates that gas estimates are loading when gasEstimateType is not FEE_MARKET but network supports EIP-1559', () => {
it('indicates that gas estimates are loading when gasEstimateType is not FEE_MARKET or ETH_GASPRICE, but network supports EIP-1559', () => {
useSelector.mockImplementation(
generateUseSelectorRouter({
isEIP1559Network: true,
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
gasFeeEstimates: {
gasPrice: '10',
},
@ -189,7 +189,7 @@ describe('useGasFeeEstimates', () => {
} = renderHook(() => useGasFeeEstimates());
expect(current).toMatchObject({
gasFeeEstimates: { gasPrice: '10' },
gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE,
gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY,
estimatedGasFeeTimeBounds: undefined,
isGasEstimatesLoading: true,
});

@ -18,6 +18,8 @@ import {
getMinimumGasTotalInHexWei,
} from '../../shared/modules/gas.utils';
import { PRIMARY, SECONDARY } from '../helpers/constants/common';
import { isEIP1559Network } from '../ducks/metamask/metamask';
import {
hexWEIToDecGWEI,
decGWEIToHexWEI,
@ -173,7 +175,7 @@ export function useGasFeeInputs(
) {
const { balance: ethBalance } = useSelector(getSelectedAccount);
const txData = useSelector(txDataSelector);
const networkSupportsEIP1559 = useSelector(isEIP1559Network);
// We need to know whether to show fiat conversions or not, so that we can
// default our fiat values to empty strings if showing fiat is not wanted or
// possible.
@ -282,12 +284,13 @@ export function useGasFeeInputs(
const gasSettings = {
gasLimit: decimalToHex(gasLimit),
};
if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) {
gasSettings.maxFeePerGas = decGWEIToHexWEI(maxFeePerGasToUse);
gasSettings.maxPriorityFeePerGas = decGWEIToHexWEI(
maxPriorityFeePerGasToUse,
);
if (networkSupportsEIP1559) {
gasSettings.maxFeePerGas = maxFeePerGasToUse
? decGWEIToHexWEI(maxFeePerGasToUse)
: decGWEIToHexWEI(gasPriceToUse || '0');
gasSettings.maxPriorityFeePerGas = maxPriorityFeePerGasToUse
? decGWEIToHexWEI(maxPriorityFeePerGasToUse)
: gasSettings.maxFeePerGas;
gasSettings.baseFeePerGas = decGWEIToHexWEI(
gasFeeEstimates.estimatedBaseFee ?? '0',
);

@ -3,9 +3,11 @@ import { useSelector } from 'react-redux';
import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas';
import { multiplyCurrencies } from '../../shared/modules/conversion.utils';
import {
isEIP1559Network,
getConversionRate,
getNativeCurrency,
} from '../ducks/metamask/metamask';
import { ETH, PRIMARY } from '../helpers/constants/common';
import {
getCurrentCurrency,
@ -102,7 +104,9 @@ const HIGH_FEE_MARKET_ESTIMATE_RETURN_VALUE = {
estimatedGasFeeTimeBounds: {},
};
const generateUseSelectorRouter = () => (selector) => {
const generateUseSelectorRouter = ({ isEIP1559NetworkResponse } = {}) => (
selector,
) => {
if (selector === getConversionRate) {
return MOCK_ETH_USD_CONVERSION_RATE;
}
@ -127,6 +131,9 @@ const generateUseSelectorRouter = () => (selector) => {
balance: '0x440aa47cc2556',
};
}
if (selector === isEIP1559Network) {
return isEIP1559NetworkResponse;
}
return undefined;
};
@ -178,6 +185,9 @@ describe('useGasFeeInputs', () => {
});
it('updates values when user modifies gasPrice', () => {
useSelector.mockImplementation(
generateUseSelectorRouter({ isEIP1559NetworkResponse: false }),
);
const { result } = renderHook(() => useGasFeeInputs());
expect(result.current.gasPrice).toBe(
LEGACY_GAS_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium,
@ -242,6 +252,9 @@ describe('useGasFeeInputs', () => {
});
it('updates values when user modifies maxFeePerGas', () => {
useSelector.mockImplementation(
generateUseSelectorRouter({ isEIP1559NetworkResponse: true }),
);
const { result } = renderHook(() => useGasFeeInputs());
expect(result.current.maxFeePerGas).toBe(
FEE_MARKET_ESTIMATE_RETURN_VALUE.gasFeeEstimates.medium
@ -297,7 +310,9 @@ describe('useGasFeeInputs', () => {
useGasFeeEstimates.mockImplementation(
() => HIGH_FEE_MARKET_ESTIMATE_RETURN_VALUE,
);
useSelector.mockImplementation(generateUseSelectorRouter());
useSelector.mockImplementation(
generateUseSelectorRouter({ isEIP1559NetworkResponse: true }),
);
});
it('should return true', () => {

@ -0,0 +1,33 @@
import { useEffect } from 'react';
import {
disconnectGasFeeEstimatePoller,
getGasFeeEstimatesAndStartPolling,
} from '../store/actions';
/**
* Provides a reusable hook that can be used for safely updating the polling
* data in the gas fee controller. It makes a request to get estimates and
* begin polling, keeping track of the poll token for the lifetime of the hook.
* It then disconnects polling upon unmount. If the hook is unmounted while waiting
* for `getGasFeeEstimatesAndStartPolling` to resolve, the `active` flag ensures
* that a call to disconnect happens after promise resolution.
*/
export function useSafeGasEstimatePolling() {
useEffect(() => {
let active = true;
let pollToken;
getGasFeeEstimatesAndStartPolling().then((newPollToken) => {
if (active) {
pollToken = newPollToken;
} else {
disconnectGasFeeEstimatePoller(newPollToken);
}
});
return () => {
active = false;
if (pollToken) {
disconnectGasFeeEstimatePoller(pollToken);
}
};
}, []);
}

@ -39,6 +39,10 @@ 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,
} from '../../store/actions';
export default class ConfirmTransactionBase extends Component {
static contextTypes = {
@ -58,7 +62,8 @@ export default class ConfirmTransactionBase extends Component {
fromAddress: PropTypes.string,
fromName: PropTypes.string,
hexTransactionAmount: PropTypes.string,
hexTransactionFee: PropTypes.string,
hexMinimumTransactionFee: PropTypes.string,
hexMaximumTransactionFee: PropTypes.string,
hexTransactionTotal: PropTypes.string,
methodData: PropTypes.object,
nonce: PropTypes.string,
@ -191,7 +196,7 @@ export default class ConfirmTransactionBase extends Component {
const {
balance,
conversionRate,
hexTransactionFee,
hexMaximumTransactionFee,
txData: { simulationFails, txParams: { value: amount } = {} } = {},
customGas,
noGasPrice,
@ -201,7 +206,7 @@ export default class ConfirmTransactionBase extends Component {
balance &&
!isBalanceSufficient({
amount,
gasTotal: hexTransactionFee || '0x0',
gasTotal: hexMaximumTransactionFee || '0x0',
balance,
conversionRate,
});
@ -282,7 +287,7 @@ export default class ConfirmTransactionBase extends Component {
const {
primaryTotalTextOverride,
secondaryTotalTextOverride,
hexTransactionFee,
hexMinimumTransactionFee,
hexTransactionTotal,
useNonceField,
customNonceValue,
@ -422,14 +427,14 @@ export default class ConfirmTransactionBase extends Component {
detailText={
<UserPreferencedCurrencyDisplay
type={PRIMARY}
value={hexTransactionFee}
value={hexMinimumTransactionFee}
hideLabel={false}
/>
}
detailTotal={
<UserPreferencedCurrencyDisplay
type={SECONDARY}
value={hexTransactionFee}
value={hexMinimumTransactionFee}
hideLabel
/>
}
@ -496,7 +501,7 @@ export default class ConfirmTransactionBase extends Component {
<div className="confirm-page-container-content__gas-fee">
<ConfirmDetailRow
label={t('gasFee')}
value={hexTransactionFee}
value={hexMinimumTransactionFee}
headerText={showGasEditButton ? t('edit') : ''}
headerTextClassName={
showGasEditButton ? 'confirm-detail-row__header-text--edit' : ''
@ -769,6 +774,7 @@ export default class ConfirmTransactionBase extends Component {
};
componentDidMount() {
this._isMounted = true;
const {
toAddress,
txData: { origin } = {},
@ -795,9 +801,28 @@ export default class ConfirmTransactionBase extends Component {
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) {
this.setState({ pollingToken });
} else {
disconnectGasFeeEstimatePoller(pollingToken);
}
});
}
componentWillUnmount() {
this._isMounted = false;
if (this.state.pollingToken) {
disconnectGasFeeEstimatePoller(this.state.pollingToken);
}
this._removeBeforeUnload();
}

@ -38,7 +38,10 @@ import {
import { getMostRecentOverviewPage } from '../../ducks/history/history';
import { transactionMatchesNetwork } from '../../../shared/modules/transaction.utils';
import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
import { updateTransactionGasFees } from '../../ducks/metamask/metamask';
import {
updateTransactionGasFees,
isEIP1559Network,
} from '../../ducks/metamask/metamask';
import ConfirmTransactionBase from './confirm-transaction-base.component';
const casedContractMap = Object.keys(contractMap).reduce((acc, base) => {
@ -65,6 +68,7 @@ const mapStateToProps = (state, ownProps) => {
} = ownProps;
const { id: paramsTransactionId } = params;
const isMainnet = getIsMainnet(state);
const supportsEIP1599 = isEIP1559Network(state);
const { confirmTransaction, metamask } = state;
const {
ensResolutionsByAddress,
@ -111,7 +115,8 @@ const mapStateToProps = (state, ownProps) => {
const {
hexTransactionAmount,
hexTransactionFee,
hexMinimumTransactionFee,
hexMaximumTransactionFee,
hexTransactionTotal,
} = transactionFeeSelector(state, transaction);
@ -158,7 +163,8 @@ const mapStateToProps = (state, ownProps) => {
toName,
toNickname,
hexTransactionAmount,
hexTransactionFee,
hexMinimumTransactionFee,
hexMaximumTransactionFee,
hexTransactionTotal,
txData: fullTxData,
tokenData,
@ -187,6 +193,7 @@ const mapStateToProps = (state, ownProps) => {
isMainnet,
isEthGasPrice,
noGasPrice,
supportsEIP1599,
};
};

@ -27,10 +27,7 @@ export default function SendHeader() {
let title = asset.type === ASSET_TYPES.NATIVE ? t('send') : t('sendTokens');
if (
stage === SEND_STAGES.ADD_RECIPIENT ||
stage === SEND_STAGES.UNINITIALIZED
) {
if (stage === SEND_STAGES.ADD_RECIPIENT || stage === SEND_STAGES.INACTIVE) {
title = t('addRecipient');
} else if (stage === SEND_STAGES.EDIT) {
title = t('edit');

@ -21,7 +21,7 @@ jest.mock('react-router-dom', () => {
describe('SendHeader Component', () => {
describe('Title', () => {
it('should render "Add Recipient" for UNINITIALIZED or ADD_RECIPIENT stages', () => {
it('should render "Add Recipient" for INACTIVE or ADD_RECIPIENT stages', () => {
const { getByText, rerender } = renderWithProvider(
<SendHeader />,
configureMockStore(middleware)({

@ -4,14 +4,25 @@ import { calcTokenAmount } from '../helpers/utils/token-util';
import {
roundExponential,
getValueFromWeiHex,
getHexGasTotal,
getTransactionFee,
addFiat,
addEth,
} from '../helpers/utils/confirm-tx.util';
import { sumHexes } from '../helpers/utils/transactions.util';
import { transactionMatchesNetwork } from '../../shared/modules/transaction.utils';
import { getNativeCurrency } from '../ducks/metamask/metamask';
import {
getGasEstimateType,
getGasFeeEstimates,
getNativeCurrency,
isEIP1559Network,
} from '../ducks/metamask/metamask';
import { TRANSACTION_ENVELOPE_TYPES } from '../../shared/constants/transaction';
import { decGWEIToHexWEI } from '../helpers/utils/conversions.util';
import { GAS_ESTIMATE_TYPES } from '../../shared/constants/gas';
import {
getMaximumGasTotalInHexWei,
getMinimumGasTotalInHexWei,
} from '../../shared/modules/gas.utils';
import { getAveragePriceEstimateInHexWEI } from './custom-gas';
import { getCurrentChainId, deprecatedGetCurrentNetworkId } from './selectors';
@ -218,17 +229,56 @@ export const transactionFeeSelector = function (state, txData) {
const currentCurrency = currentCurrencySelector(state);
const conversionRate = conversionRateSelector(state);
const nativeCurrency = getNativeCurrency(state);
const gasFeeEstimates = getGasFeeEstimates(state) || {};
const gasEstimateType = getGasEstimateType(state);
const networkSupportsEIP1559 = isEIP1559Network(state);
const { txParams: { value = '0x0', gas: gasLimit = '0x0' } = {} } = txData;
// if the gas price from our infura endpoint is null or undefined
// use the metaswap average price estimation as a fallback
let { txParams: { gasPrice } = {} } = txData;
const gasEstimationObject = {
gasLimit: txData.txParams?.gas ?? '0x0',
};
if (!gasPrice) {
gasPrice = getAveragePriceEstimateInHexWEI(state) || '0x0';
if (networkSupportsEIP1559) {
const { medium = {}, gasPrice = '0' } = gasFeeEstimates;
if (txData.txParams?.type === TRANSACTION_ENVELOPE_TYPES.LEGACY) {
gasEstimationObject.gasPrice =
txData.txParams?.gasPrice ?? decGWEIToHexWEI(gasPrice);
} else {
const { suggestedMaxPriorityFeePerGas, suggestedMaxFeePerGas } = medium;
gasEstimationObject.maxFeePerGas =
txData.txParams?.maxFeePerGas ??
decGWEIToHexWEI(suggestedMaxFeePerGas || gasPrice);
gasEstimationObject.maxPriorityFeePerGas =
txData.txParams?.maxPriorityFeePerGas ??
((suggestedMaxPriorityFeePerGas &&
decGWEIToHexWEI(suggestedMaxPriorityFeePerGas)) ||
gasEstimationObject.maxFeePerGas);
gasEstimationObject.baseFeePerGas = decGWEIToHexWEI(
gasFeeEstimates.estimatedBaseFee,
);
}
} else {
switch (gasEstimateType) {
case GAS_ESTIMATE_TYPES.NONE:
gasEstimationObject.gasPrice = txData.txParams?.gasPrice ?? '0x0';
break;
case GAS_ESTIMATE_TYPES.ETH_GASPRICE:
gasEstimationObject.gasPrice =
txData.txParams?.gasPrice ??
decGWEIToHexWEI(gasFeeEstimates.gasPrice);
break;
case GAS_ESTIMATE_TYPES.LEGACY:
gasEstimationObject.gasPrice =
txData.txParams?.gasPrice ?? getAveragePriceEstimateInHexWEI(state);
break;
case GAS_ESTIMATE_TYPES.FEE_MARKET:
break;
default:
break;
}
}
const { txParams: { value = '0x0' } = {} } = txData;
const fiatTransactionAmount = getValueFromWeiHex({
value,
fromCurrency: nativeCurrency,
@ -244,17 +294,31 @@ export const transactionFeeSelector = function (state, txData) {
numberOfDecimals: 6,
});
const hexTransactionFee = getHexGasTotal({ gasLimit, gasPrice });
const hexMinimumTransactionFee = getMinimumGasTotalInHexWei(
gasEstimationObject,
);
const hexMaximumTransactionFee = getMaximumGasTotalInHexWei(
gasEstimationObject,
);
const fiatMinimumTransactionFee = getTransactionFee({
value: hexMinimumTransactionFee,
fromCurrency: nativeCurrency,
toCurrency: currentCurrency,
numberOfDecimals: 2,
conversionRate,
});
const fiatTransactionFee = getTransactionFee({
value: hexTransactionFee,
const fiatMaximumTransactionFee = getTransactionFee({
value: hexMaximumTransactionFee,
fromCurrency: nativeCurrency,
toCurrency: currentCurrency,
numberOfDecimals: 2,
conversionRate,
});
const ethTransactionFee = getTransactionFee({
value: hexTransactionFee,
value: hexMinimumTransactionFee,
fromCurrency: nativeCurrency,
toCurrency: nativeCurrency,
numberOfDecimals: 6,
@ -262,18 +326,20 @@ export const transactionFeeSelector = function (state, txData) {
});
const fiatTransactionTotal = addFiat(
fiatTransactionFee,
fiatMinimumTransactionFee,
fiatTransactionAmount,
);
const ethTransactionTotal = addEth(ethTransactionFee, ethTransactionAmount);
const hexTransactionTotal = sumHexes(value, hexTransactionFee);
const hexTransactionTotal = sumHexes(value, hexMinimumTransactionFee);
return {
hexTransactionAmount: value,
fiatTransactionAmount,
ethTransactionAmount,
hexTransactionFee,
fiatTransactionFee,
hexMinimumTransactionFee,
fiatMinimumTransactionFee,
hexMaximumTransactionFee,
fiatMaximumTransactionFee,
ethTransactionFee,
fiatTransactionTotal,
ethTransactionTotal,

Loading…
Cancel
Save