diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 0880afab0fb..c43ffb40acd 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The `userFeeLevel` for such transactions is set to `medium` instead of `dappSuggested`, so the values are kept updated while the transaction is unapproved. - The original dapp-suggested values remain available via `dappSuggestedGasFees` on the transaction metadata. - Disabled by default. +- Ignore underpriced saved (advanced) gas fee preferences in favour of the wallet's suggested estimates ([#9705](https://github.com/MetaMask/core/pull/9705)) + - If the new `replaceUnderpricedSavedGasFees` feature flag is enabled for the chain, saved custom fees with a `maxBaseFee` below the current low estimate are ignored and the suggested medium values are used instead, as they are unlikely to result in inclusion in a block before fee values change. + - Level-based saved preferences track current estimates and are never ignored. + - The `userFeeLevel` for such transactions is set to `medium` instead of `custom`, so the values are kept updated while the transaction is unapproved. + - Disabled by default. ## [69.3.0] diff --git a/packages/transaction-controller/src/utils/feature-flags.ts b/packages/transaction-controller/src/utils/feature-flags.ts index b7852ba559e..a1c4d215813 100644 --- a/packages/transaction-controller/src/utils/feature-flags.ts +++ b/packages/transaction-controller/src/utils/feature-flags.ts @@ -205,6 +205,27 @@ export type TransactionControllerFeatureFlags = { */ default?: boolean; }; + + /** + * Replacement of underpriced saved (advanced) gas fee preferences. + * If enabled, saved custom fees with a `maxBaseFee` below the current low + * estimate are ignored in favour of the wallet's suggested fees, as they + * are unlikely to be included in a block before fee values change. + * Level-based saved preferences track current estimates and are never + * ignored. + */ + replaceUnderpricedSavedGasFees?: { + /** Enablement on a per-chain basis. */ + perChainConfig?: { + [chainId: Hex]: boolean; + }; + + /** + * Default enablement. + * This value is used when no specific value is found for a chain ID. + */ + default?: boolean; + }; }; }; @@ -508,6 +529,30 @@ export function getReplaceUnderpricedDappGasFeesEnabled( ); } +/** + * Retrieves whether underpriced saved (advanced) gas fee preferences should be + * ignored in favour of the wallet's suggested fees. + * + * @param chainId - The chain ID. + * @param messenger - The controller messenger instance. + * @returns Whether the replacement is enabled. + */ +export function getReplaceUnderpricedSavedGasFeesEnabled( + chainId: Hex, + messenger: TransactionControllerMessenger, +): boolean { + const featureFlags = getFeatureFlags(messenger); + + const replaceUnderpricedSavedGasFeesFlags = + featureFlags?.[FeatureFlag.Transactions]?.replaceUnderpricedSavedGasFees; + + return ( + replaceUnderpricedSavedGasFeesFlags?.perChainConfig?.[chainId] ?? + replaceUnderpricedSavedGasFeesFlags?.default ?? + false + ); +} + /** * Retrieves the relevant feature flags from the remote feature flag controller. * diff --git a/packages/transaction-controller/src/utils/gas-fees.test.ts b/packages/transaction-controller/src/utils/gas-fees.test.ts index 92af7fddd17..e6debc29e08 100644 --- a/packages/transaction-controller/src/utils/gas-fees.test.ts +++ b/packages/transaction-controller/src/utils/gas-fees.test.ts @@ -8,7 +8,10 @@ import { TransactionType, UserFeeLevel, } from '../types.js'; -import { getReplaceUnderpricedDappGasFeesEnabled } from './feature-flags.js'; +import { + getReplaceUnderpricedDappGasFeesEnabled, + getReplaceUnderpricedSavedGasFeesEnabled, +} from './feature-flags.js'; import type { UpdateGasFeesRequest } from './gas-fees.js'; import { gweiDecimalToWeiDecimal, updateGasFees } from './gas-fees.js'; import { rpcRequest } from './provider.js'; @@ -107,6 +110,9 @@ describe('gas-fees', () => { const getReplaceUnderpricedDappGasFeesEnabledMock = jest.mocked( getReplaceUnderpricedDappGasFeesEnabled, ); + const getReplaceUnderpricedSavedGasFeesEnabledMock = jest.mocked( + getReplaceUnderpricedSavedGasFeesEnabled, + ); let gasFeeFlowMock: jest.Mocked; /** @@ -863,6 +869,98 @@ describe('gas-fees', () => { ); }); }); + + describe('replaces underpriced saved gas fees', () => { + beforeEach(() => { + getReplaceUnderpricedSavedGasFeesEnabledMock.mockReturnValue(true); + mockGasFeeFlowMockResponse(FLOW_RESPONSE_FEE_MARKET_MOCK); + }); + + it('with suggested medium values if saved maxBaseFee below low estimate', async () => { + updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({ + maxBaseFee: String(GAS_LOW_MOCK - 1), + priorityFee: '1', + }); + + await updateGasFees(updateGasFeeRequest); + + expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe( + GAS_HEX_WEI_MOCK, + ); + expect(updateGasFeeRequest.txMeta.txParams.maxPriorityFeePerGas).toBe( + GAS_HEX_WEI_MOCK, + ); + expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe( + UserFeeLevel.MEDIUM, + ); + }); + + it('unless saved maxBaseFee is at or above low estimate', async () => { + updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({ + maxBaseFee: String(GAS_LOW_MOCK), + priorityFee: '1', + }); + + await updateGasFees(updateGasFeeRequest); + + expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe( + GAS_LOW_HEX_WEI_MOCK, + ); + expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe( + UserFeeLevel.CUSTOM, + ); + }); + + it('unless saved preference is level-based', async () => { + updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({ + level: GasFeeEstimateLevel.Low, + }); + + await updateGasFees(updateGasFeeRequest); + + expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe( + GAS_LOW_HEX_WEI_MOCK, + ); + expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe( + GasFeeEstimateLevel.Low, + ); + }); + + it('unless feature flag is disabled', async () => { + getReplaceUnderpricedSavedGasFeesEnabledMock.mockReturnValue(false); + updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({ + maxBaseFee: String(GAS_LOW_MOCK - 1), + priorityFee: '1', + }); + + await updateGasFees(updateGasFeeRequest); + + expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe( + UNDERPRICED_GAS_HEX_WEI_MOCK, + ); + expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe( + UserFeeLevel.CUSTOM, + ); + }); + + it('unless estimates are unavailable', async () => { + gasFeeFlowMock.getGasFees.mockRejectedValue(new Error('TestError')); + rpcRequestMock.mockResolvedValue(GAS_HEX_WEI_MOCK); + updateGasFeeRequest.getSavedGasFees.mockReturnValueOnce({ + maxBaseFee: String(GAS_LOW_MOCK - 1), + priorityFee: '1', + }); + + await updateGasFees(updateGasFeeRequest); + + expect(updateGasFeeRequest.txMeta.txParams.maxFeePerGas).toBe( + UNDERPRICED_GAS_HEX_WEI_MOCK, + ); + expect(updateGasFeeRequest.txMeta.userFeeLevel).toBe( + UserFeeLevel.CUSTOM, + ); + }); + }); }); }); diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index 9becc87316c..1746ba62c77 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -21,7 +21,10 @@ import { TransactionType, UserFeeLevel, } from '../types.js'; -import { getReplaceUnderpricedDappGasFeesEnabled } from './feature-flags.js'; +import { + getReplaceUnderpricedDappGasFeesEnabled, + getReplaceUnderpricedSavedGasFeesEnabled, +} from './feature-flags.js'; import { getGasFeeFlow } from './gas-flow.js'; import { rpcRequest } from './provider.js'; import { SWAP_TRANSACTION_TYPES } from './swaps.js'; @@ -85,7 +88,7 @@ export async function updateGasFees( txMeta.type as TransactionType, ); - const savedGasFees = + let savedGasFees = shouldIgnoreSavedGasFees || hasInitialGasFeeParams(initialParams) ? undefined : request.getSavedGasFees(txMeta); @@ -97,6 +100,17 @@ export async function updateGasFees( log('Suggested gas fees', suggestedGasFees); + if ( + shouldIgnoreUnderpricedSavedGasFees(savedGasFees, suggestedGasFees, request) + ) { + log( + 'Ignoring saved custom gas fees below low estimate', + savedGasFees?.maxBaseFee, + suggestedGasFees.low?.maxFeePerGas, + ); + savedGasFees = undefined; + } + const getGasFeeRequest: GetGasFeeRequest = { ...request, initialParams, @@ -157,6 +171,53 @@ export function gweiDecimalToWeiDecimal(gweiDecimal: string | number): string { return weiValue.toString().split('.')[0]; } +/** + * Determine whether saved (advanced) gas fee preferences should be ignored in + * favour of the suggested gas fees, as the saved custom `maxBaseFee` is below + * the current low estimate and hence unlikely to result in timely inclusion in + * a block. Level-based saved preferences track current estimates and are never + * ignored. + * + * @param savedGasFees - The saved gas fee preferences. + * @param suggestedGasFees - The suggested gas fees. + * @param request - The request object. + * @returns Whether the saved gas fee preferences should be ignored. + */ +function shouldIgnoreUnderpricedSavedGasFees( + savedGasFees: SavedGasFees | undefined, + suggestedGasFees: SuggestedGasFees, + request: UpdateGasFeesRequest, +): boolean { + const { eip1559, messenger, txMeta } = request; + + if ( + !eip1559 || + !savedGasFees?.maxBaseFee || + !getReplaceUnderpricedSavedGasFeesEnabled(txMeta.chainId, messenger) + ) { + return false; + } + + const lowMaxFeePerGas = suggestedGasFees.low?.maxFeePerGas; + + const hasReplacement = + Boolean(suggestedGasFees.maxFeePerGas) && + Boolean(suggestedGasFees.maxPriorityFeePerGas); + + if (!lowMaxFeePerGas || !hasReplacement) { + return false; + } + + try { + return ( + BigInt(gweiDecimalToWeiHex(savedGasFees.maxBaseFee)) < + BigInt(lowMaxFeePerGas) + ); + } catch { + return false; + } +} + /** * Determine whether dapp-suggested gas fees should be ignored in favour of the * suggested gas fees, as the dapp-suggested `maxFeePerGas` is below the current