mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
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
This commit is contained in:
parent
f1140087b7
commit
e0953d9f68
@ -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
|
||||
},
|
||||
};
|
||||
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 action = {
|
||||
type: 'send/updateDraftTransaction',
|
||||
};
|
||||
|
||||
const result = sendReducer(detailsForDraftTransactionState, action);
|
||||
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
|
||||
},
|
||||
};
|
||||
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 action = {
|
||||
type: 'send/updateDraftTransaction',
|
||||
};
|
||||
|
||||
const result = sendReducer(detailsForDraftTransactionState, action);
|
||||
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',
|
||||
);
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
33
ui/hooks/useSafeGasEstimatePolling.js
Normal file
33
ui/hooks/useSafeGasEstimatePolling.js
Normal file
@ -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;
|
||||
const gasEstimationObject = {
|
||||
gasLimit: txData.txParams?.gas ?? '0x0',
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
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 fiatTransactionFee = getTransactionFee({
|
||||
value: hexTransactionFee,
|
||||
const fiatMinimumTransactionFee = getTransactionFee({
|
||||
value: hexMinimumTransactionFee,
|
||||
fromCurrency: nativeCurrency,
|
||||
toCurrency: currentCurrency,
|
||||
numberOfDecimals: 2,
|
||||
conversionRate,
|
||||
});
|
||||
|
||||
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…
Reference in New Issue
Block a user