From d88ac99248b7526b7244da3198cb35fe6f707290 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 12:19:42 +0100 Subject: [PATCH 01/24] Add RandomisedEstimationsGasFeeFlow --- .../src/TransactionController.test.ts | 26 +- .../src/TransactionController.ts | 13 +- .../src/gas-flows/DefaultGasFeeFlow.test.ts | 7 + .../src/gas-flows/DefaultGasFeeFlow.ts | 2 +- .../RandomisedEstimationsGasFeeFlow.test.ts | 366 ++++++++++++++++++ .../RandomisedEstimationsGasFeeFlow.ts | 270 +++++++++++++ .../src/helpers/GasFeePoller.test.ts | 21 + .../src/helpers/GasFeePoller.ts | 16 +- packages/transaction-controller/src/types.ts | 10 +- .../src/utils/feature-flags.ts | 13 +- .../src/utils/gas-fees.ts | 19 +- .../src/utils/gas-flow.test.ts | 19 +- .../src/utils/gas-flow.ts | 5 +- 13 files changed, 774 insertions(+), 13 deletions(-) create mode 100644 packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts create mode 100644 packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 7a108a3d605..bc17eb95e08 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -39,6 +39,7 @@ import { getAccountAddressRelationship } from './api/accounts-api'; import { CHAIN_IDS } from './constants'; import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow'; import { LineaGasFeeFlow } from './gas-flows/LineaGasFeeFlow'; +import { RandomisedEstimationsGasFeeFlow } from './gas-flows/RandomisedEstimationsGasFeeFlow'; import { TestGasFeeFlow } from './gas-flows/TestGasFeeFlow'; import { GasFeePoller } from './helpers/GasFeePoller'; import { IncomingTransactionHelper } from './helpers/IncomingTransactionHelper'; @@ -115,6 +116,7 @@ const ORIGIN_MOCK = 'test.com'; jest.mock('@metamask/eth-query'); jest.mock('./api/accounts-api'); jest.mock('./gas-flows/DefaultGasFeeFlow'); +jest.mock('./gas-flows/RandomisedEstimationsGasFeeFlow'); jest.mock('./gas-flows/LineaGasFeeFlow'); jest.mock('./gas-flows/TestGasFeeFlow'); jest.mock('./helpers/GasFeePoller'); @@ -514,6 +516,9 @@ describe('TransactionController', () => { ); const defaultGasFeeFlowClassMock = jest.mocked(DefaultGasFeeFlow); const lineaGasFeeFlowClassMock = jest.mocked(LineaGasFeeFlow); + const randomisedEstimationsGasFeeFlowClassMock = jest.mocked( + RandomisedEstimationsGasFeeFlow, + ); const testGasFeeFlowClassMock = jest.mocked(TestGasFeeFlow); const gasFeePollerClassMock = jest.mocked(GasFeePoller); const getSimulationDataMock = jest.mocked(getSimulationData); @@ -536,6 +541,7 @@ describe('TransactionController', () => { let multichainTrackingHelperMock: jest.Mocked; let defaultGasFeeFlowMock: jest.Mocked; let lineaGasFeeFlowMock: jest.Mocked; + let randomisedEstimationsGasFeeFlowMock: jest.Mocked; let testGasFeeFlowMock: jest.Mocked; let gasFeePollerMock: jest.Mocked; let methodDataHelperMock: jest.Mocked; @@ -919,6 +925,13 @@ describe('TransactionController', () => { return lineaGasFeeFlowMock; }); + randomisedEstimationsGasFeeFlowClassMock.mockImplementation(() => { + randomisedEstimationsGasFeeFlowMock = { + matchesTransaction: () => false, + } as unknown as jest.Mocked; + return randomisedEstimationsGasFeeFlowMock; + }); + testGasFeeFlowClassMock.mockImplementation(() => { testGasFeeFlowMock = { matchesTransaction: () => false, @@ -971,7 +984,11 @@ describe('TransactionController', () => { expect(gasFeePollerClassMock).toHaveBeenCalledTimes(1); expect(gasFeePollerClassMock).toHaveBeenCalledWith( expect.objectContaining({ - gasFeeFlows: [lineaGasFeeFlowMock, defaultGasFeeFlowMock], + gasFeeFlows: [ + randomisedEstimationsGasFeeFlowMock, + lineaGasFeeFlowMock, + defaultGasFeeFlowMock, + ], }), ); }); @@ -2018,7 +2035,12 @@ describe('TransactionController', () => { expect(updateGasFeesMock).toHaveBeenCalledWith({ eip1559: true, ethQuery: expect.any(Object), - gasFeeFlows: [lineaGasFeeFlowMock, defaultGasFeeFlowMock], + featureFlags: expect.any(Object), + gasFeeFlows: [ + randomisedEstimationsGasFeeFlowMock, + lineaGasFeeFlowMock, + defaultGasFeeFlowMock, + ], getGasFeeEstimates: expect.any(Function), getSavedGasFees: expect.any(Function), txMeta: expect.any(Object), diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index 68aa626983e..fe6e24ec81c 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -60,6 +60,7 @@ import { import { DefaultGasFeeFlow } from './gas-flows/DefaultGasFeeFlow'; import { LineaGasFeeFlow } from './gas-flows/LineaGasFeeFlow'; import { OptimismLayer1GasFeeFlow } from './gas-flows/OptimismLayer1GasFeeFlow'; +import { RandomisedEstimationsGasFeeFlow } from './gas-flows/RandomisedEstimationsGasFeeFlow'; import { ScrollLayer1GasFeeFlow } from './gas-flows/ScrollLayer1GasFeeFlow'; import { TestGasFeeFlow } from './gas-flows/TestGasFeeFlow'; import { AccountsApiRemoteTransactionSource } from './helpers/AccountsApiRemoteTransactionSource'; @@ -116,6 +117,7 @@ import { signAuthorizationList, } from './utils/eip7702'; import { validateConfirmedExternalTransaction } from './utils/external-transactions'; +import { getFeatureFlags } from './utils/feature-flags'; import { addGasBuffer, estimateGas, updateGas } from './utils/gas'; import { updateGasFees } from './utils/gas-fees'; import { getGasFeeFlow } from './utils/gas-flow'; @@ -912,6 +914,7 @@ export class TransactionController extends BaseController< getProvider: (networkClientId) => this.#getProvider({ networkClientId }), getTransactions: () => this.state.transactions, layer1GasFeeFlows: this.layer1GasFeeFlows, + messenger: this.messagingSystem, onStateChange: (listener) => { this.messagingSystem.subscribe( 'TransactionController:stateChange', @@ -2283,6 +2286,7 @@ export class TransactionController extends BaseController< networkClientId: requestNetworkClientId, }); + const featureFlags = getFeatureFlags(this.messagingSystem); const transactionMeta = { txParams: transactionParams, chainId, @@ -2293,6 +2297,7 @@ export class TransactionController extends BaseController< const gasFeeFlow = getGasFeeFlow( transactionMeta, this.gasFeeFlows, + featureFlags, ) as GasFeeFlow; const ethQuery = new EthQuery(provider); @@ -2303,6 +2308,7 @@ export class TransactionController extends BaseController< return gasFeeFlow.getGasFees({ ethQuery, + featureFlags, gasFeeControllerData, transactionMeta, }); @@ -2578,6 +2584,7 @@ export class TransactionController extends BaseController< await updateGasFees({ eip1559: isEIP1559Compatible, ethQuery, + featureFlags: getFeatureFlags(this.messagingSystem), gasFeeFlows: this.gasFeeFlows, getGasFeeEstimates: this.getGasFeeEstimates, getSavedGasFees: this.getSavedGasFees.bind(this), @@ -3749,7 +3756,11 @@ export class TransactionController extends BaseController< return [new TestGasFeeFlow()]; } - return [new LineaGasFeeFlow(), new DefaultGasFeeFlow()]; + return [ + new RandomisedEstimationsGasFeeFlow(), + new LineaGasFeeFlow(), + new DefaultGasFeeFlow(), + ]; } #getLayer1GasFeeFlows(): Layer1GasFeeFlow[] { diff --git a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts index 1965cbb3a57..09076c0c60f 100644 --- a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts @@ -8,6 +8,7 @@ import type { import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; +import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; import type { FeeMarketGasFeeEstimates, GasPriceGasFeeEstimates, @@ -50,6 +51,8 @@ const LEGACY_ESTIMATES_MOCK: LegacyGasPriceEstimate = { high: '5', }; +const FEATURE_FLAGS_MOCK = {} as TransactionControllerFeatureFlags; + const FEE_MARKET_RESPONSE_MOCK = { gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, gasFeeEstimates: FEE_MARKET_ESTIMATES_MOCK, @@ -112,6 +115,7 @@ describe('DefaultGasFeeFlow', () => { const response = await defaultGasFeeFlow.getGasFees({ ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, gasFeeControllerData: FEE_MARKET_RESPONSE_MOCK, transactionMeta: TRANSACTION_META_MOCK, }); @@ -126,6 +130,7 @@ describe('DefaultGasFeeFlow', () => { const response = await defaultGasFeeFlow.getGasFees({ ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, gasFeeControllerData: LEGACY_RESPONSE_MOCK, transactionMeta: TRANSACTION_META_MOCK, }); @@ -140,6 +145,7 @@ describe('DefaultGasFeeFlow', () => { const response = await defaultGasFeeFlow.getGasFees({ ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, gasFeeControllerData: GAS_PRICE_RESPONSE_MOCK, transactionMeta: TRANSACTION_META_MOCK, }); @@ -154,6 +160,7 @@ describe('DefaultGasFeeFlow', () => { const response = defaultGasFeeFlow.getGasFees({ ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, gasFeeControllerData: { gasEstimateType: GAS_ESTIMATE_TYPES.NONE, } as GasFeeState, diff --git a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts index b708145535a..e5d9c96f84f 100644 --- a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts @@ -57,7 +57,7 @@ export class DefaultGasFeeFlow implements GasFeeFlow { break; default: // TODO: Either fix this lint violation or explain why it's necessary to ignore. - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unsupported gas estimate type: ${gasEstimateType}`); } diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts new file mode 100644 index 00000000000..988b93cdaa5 --- /dev/null +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -0,0 +1,366 @@ +import { toHex } from '@metamask/controller-utils'; +import type EthQuery from '@metamask/eth-query'; +import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; +import type { GasFeeState } from '@metamask/gas-fee-controller'; + +import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; +import { RandomisedEstimationsGasFeeFlow } from './RandomisedEstimationsGasFeeFlow'; +import type { + FeeMarketGasFeeEstimates, + GasPriceGasFeeEstimates, + LegacyGasFeeEstimates, + TransactionMeta, +} from '../types'; +import { + GasFeeEstimateLevel, + GasFeeEstimateType, + TransactionStatus, +} from '../types'; +import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; +import { FEATURE_FLAG_RANDOMISE_GAS_FEES } from '../utils/feature-flags'; + +jest.mock('./DefaultGasFeeFlow'); + +// Mock Math.random to return predictable values +const originalRandom = global.Math.random; +jest.spyOn(global.Math, 'random').mockReturnValue(0.5); + +const TRANSACTION_META_MOCK: TransactionMeta = { + id: '1', + chainId: '0x1', + networkClientId: 'testNetworkClientId', + status: TransactionStatus.unapproved, + time: 0, + txParams: { + from: '0x123', + }, +}; + +const FEATURE_FLAGS_MOCK: TransactionControllerFeatureFlags = { + [FEATURE_FLAG_RANDOMISE_GAS_FEES]: { + config: { + '0x1': 6, + '0x5': 4, + }, + }, +}; + +const ETH_QUERY_MOCK = {} as EthQuery; + +const DEFAULT_FEE_MARKET_RESPONSE: FeeMarketGasFeeEstimates = { + type: GasFeeEstimateType.FeeMarket, + low: { + maxFeePerGas: toHex(1e9), + maxPriorityFeePerGas: toHex(2e9), + }, + medium: { + maxFeePerGas: toHex(3e9), + maxPriorityFeePerGas: toHex(4e9), + }, + high: { + maxFeePerGas: toHex(5e9), + maxPriorityFeePerGas: toHex(6e9), + }, +}; + +const DEFAULT_LEGACY_RESPONSE: LegacyGasFeeEstimates = { + type: GasFeeEstimateType.Legacy, + low: toHex(1e9), + medium: toHex(3e9), + high: toHex(5e9), +}; + +const DEFAULT_GAS_PRICE_RESPONSE: GasPriceGasFeeEstimates = { + type: GasFeeEstimateType.GasPrice, + gasPrice: toHex(3e9), +}; + +describe('RandomisedEstimationsGasFeeFlow', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest + .mocked(DefaultGasFeeFlow.prototype.getGasFees) + .mockImplementation(async (request) => { + const { gasFeeControllerData } = request; + if ( + gasFeeControllerData.gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET + ) { + return { estimates: DEFAULT_FEE_MARKET_RESPONSE }; + } else if ( + gasFeeControllerData.gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY + ) { + return { estimates: DEFAULT_LEGACY_RESPONSE }; + } + return { estimates: DEFAULT_GAS_PRICE_RESPONSE }; + }); + }); + + afterEach(() => { + global.Math.random = originalRandom; + }); + + describe('matchesTransaction', () => { + it('returns true if chainId exists in the feature flag config', () => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const transaction = { + ...TRANSACTION_META_MOCK, + chainId: '0x1', + } as TransactionMeta; + + expect(flow.matchesTransaction(transaction, FEATURE_FLAGS_MOCK)).toBe( + true, + ); + }); + + it('returns false if chainId is not in the randomisation config', () => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const transaction = { + ...TRANSACTION_META_MOCK, + chainId: '0x89', // Not in config + } as TransactionMeta; + + expect(flow.matchesTransaction(transaction, FEATURE_FLAGS_MOCK)).toBe( + false, + ); + }); + + it('returns false if feature flag is not exists', () => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const transaction = { + ...TRANSACTION_META_MOCK, + chainId: '0x89', // Not in config + } as TransactionMeta; + + expect( + flow.matchesTransaction( + transaction, + undefined as unknown as TransactionControllerFeatureFlags, + ), + ).toBe(false); + }); + }); + + // ... existing code ... + + describe('getGasFees', () => { + it('randomises fee market estimates for chain IDs in the feature flag config', async () => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const request = { + ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, + transactionMeta: TRANSACTION_META_MOCK, + gasFeeControllerData: { + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + gasFeeEstimates: { + low: { + suggestedMaxFeePerGas: '100000', + suggestedMaxPriorityFeePerGas: '100000', + }, + medium: { + suggestedMaxFeePerGas: '200000', + suggestedMaxPriorityFeePerGas: '200000', + }, + high: { + suggestedMaxFeePerGas: '300000', + suggestedMaxPriorityFeePerGas: '300000', + }, + }, + estimatedGasFeeTimeBounds: {}, + } as GasFeeState, + }; + + const result = await flow.getGasFees(request); + + expect(result.estimates.type).toBe(GasFeeEstimateType.FeeMarket); + + // For all levels, verify that randomization occurred but stayed within expected range + for (const level of Object.values(GasFeeEstimateLevel)) { + const estimates = request.gasFeeControllerData + .gasFeeEstimates as Record< + GasFeeEstimateLevel, + { + suggestedMaxFeePerGas: string; + suggestedMaxPriorityFeePerGas: string; + } + >; + + const maxFeeHex = (result.estimates as FeeMarketGasFeeEstimates)[level] + .maxFeePerGas; + + // Get the actual value for comparison only + const originalValue = Number(estimates[level].suggestedMaxFeePerGas); + const actualValue = parseInt(maxFeeHex.slice(2), 16) / 1e9; + + // Just verify the value changed and is within range + expect(actualValue).not.toBe(originalValue); + expect(actualValue).toBeGreaterThanOrEqual(originalValue); + + // For 6 digits randomization in FEATURE_FLAGS_MOCK for '0x1' + expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); + + // Same approach for maxPriorityFeePerGas + const maxPriorityFeeHex = ( + result.estimates as FeeMarketGasFeeEstimates + )[level].maxPriorityFeePerGas; + const originalPriorityValue = Number( + estimates[level].suggestedMaxPriorityFeePerGas, + ); + const actualPriorityValue = + parseInt(maxPriorityFeeHex.slice(2), 16) / 1e9; + + expect(actualPriorityValue).not.toBe(originalPriorityValue); + expect(actualPriorityValue).toBeGreaterThanOrEqual( + originalPriorityValue, + ); + expect(actualPriorityValue).toBeLessThanOrEqual( + originalPriorityValue + 999999, + ); + } + }); + + it('randomises legacy estimates with specified digits', async () => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const request = { + ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, + // Using 0x5 with 4 digits randomization + transactionMeta: { + ...TRANSACTION_META_MOCK, + chainId: '0x5', + } as TransactionMeta, + gasFeeControllerData: { + gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, + gasFeeEstimates: { + low: '100000', + medium: '200000', + high: '300000', + }, + } as GasFeeState, + }; + + const result = await flow.getGasFees(request); + + // Verify result type + expect(result.estimates.type).toBe(GasFeeEstimateType.Legacy); + + // For all levels, verify that randomization occurred but stayed within expected range + for (const level of Object.values(GasFeeEstimateLevel)) { + const gasHex = (result.estimates as LegacyGasFeeEstimates)[level]; + const estimates = request.gasFeeControllerData + .gasFeeEstimates as Record; + + // Convert hex to decimal for easier comparison + const originalValue = Number(estimates[level]); + const actualValue = parseInt(gasHex.slice(2), 16) / 1e9; + + // Verify value is within expected range + expect(actualValue).not.toBe(originalValue); + expect(actualValue).toBeGreaterThanOrEqual(originalValue); + // For 4 digits randomization (defined in FEATURE_FLAGS_MOCK for '0x5') + expect(actualValue).toBeLessThanOrEqual(originalValue + 9999); + } + }); + + it('randomises eth_gasPrice estimates', async () => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const request = { + ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, + transactionMeta: TRANSACTION_META_MOCK, + gasFeeControllerData: { + gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, + gasFeeEstimates: { + gasPrice: '200000', + }, + } as GasFeeState, + }; + + const result = await flow.getGasFees(request); + + // Verify result type + expect(result.estimates.type).toBe(GasFeeEstimateType.GasPrice); + + const gasHex = (result.estimates as GasPriceGasFeeEstimates).gasPrice; + const originalValue = 200000; + const actualValue = parseInt(gasHex.slice(2), 16) / 1e9; + + // Verify gas price is within expected range + expect(actualValue).not.toBe(originalValue); + expect(actualValue).toBeGreaterThanOrEqual(originalValue); + // For 6 digits randomization (defined in FEATURE_FLAGS_MOCK for '0x1') + expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); + }); + + it('should fall back to default flow if randomization fails', async () => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + // Mock Math.random to throw an error + jest.spyOn(global.Math, 'random').mockImplementation(() => { + throw new Error('Random error'); + }); + + const request = { + ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, + transactionMeta: TRANSACTION_META_MOCK, + gasFeeControllerData: { + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + gasFeeEstimates: { + low: { + suggestedMaxFeePerGas: '10', + suggestedMaxPriorityFeePerGas: '1', + }, + medium: { + suggestedMaxFeePerGas: '20', + suggestedMaxPriorityFeePerGas: '2', + }, + high: { + suggestedMaxFeePerGas: '30', + suggestedMaxPriorityFeePerGas: '3', + }, + }, + estimatedGasFeeTimeBounds: {}, + } as GasFeeState, + }; + + const result = await flow.getGasFees(request); + + // Verify that DefaultGasFeeFlow was called + expect(DefaultGasFeeFlow.prototype.getGasFees).toHaveBeenCalledWith( + request, + ); + expect(result.estimates).toEqual(DEFAULT_FEE_MARKET_RESPONSE); + }); + + it('should throw an error for unsupported gas estimate types', async () => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const request = { + ethQuery: ETH_QUERY_MOCK, + featureFlags: FEATURE_FLAGS_MOCK, + transactionMeta: TRANSACTION_META_MOCK, + gasFeeControllerData: { + gasEstimateType: 'UNSUPPORTED_TYPE', + gasFeeEstimates: {}, + } as unknown as GasFeeState, + }; + + // Capture the error in a spy so we can verify default flow was called + const spy = jest.spyOn(console, 'error').mockImplementation(); + + const result = await flow.getGasFees(request); + + expect(DefaultGasFeeFlow.prototype.getGasFees).toHaveBeenCalledWith( + request, + ); + expect(result.estimates).toEqual(DEFAULT_GAS_PRICE_RESPONSE); + spy.mockRestore(); + }); + }); +}); diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts new file mode 100644 index 00000000000..d8284cb66e8 --- /dev/null +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -0,0 +1,270 @@ +import type { + LegacyGasPriceEstimate, + GasFeeEstimates as FeeMarketGasPriceEstimate, + EthGasPriceEstimate, +} from '@metamask/gas-fee-controller'; +import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; +import { createModuleLogger, type Hex } from '@metamask/utils'; +import BN from 'bn.js'; + +import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; +import { projectLogger } from '../logger'; +import type { + FeeMarketGasFeeEstimateForLevel, + FeeMarketGasFeeEstimates, + GasFeeEstimates, + GasFeeFlow, + GasFeeFlowRequest, + GasFeeFlowResponse, + GasPriceGasFeeEstimates, + LegacyGasFeeEstimates, + TransactionMeta, +} from '../types'; +import { GasFeeEstimateLevel, GasFeeEstimateType } from '../types'; +import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; +import { FEATURE_FLAG_RANDOMISE_GAS_FEES } from '../utils/feature-flags'; +import { gweiDecimalToWeiHex } from '../utils/gas-fees'; + +const log = createModuleLogger( + projectLogger, + 'randomised-estimation-gas-fee-flow', +); + +/** + * Implementation of a gas fee flow that randomises the last digits of gas fee estimations + */ +export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { + matchesTransaction( + transactionMeta: TransactionMeta, + featureFlags: TransactionControllerFeatureFlags, + ): boolean { + const { chainId } = transactionMeta; + + const randomiseGasFeesConfig = getRandomisedGasFeeConfig(featureFlags); + + const enabledChainIds = Object.keys(randomiseGasFeesConfig); + + return enabledChainIds.includes(chainId); + } + + async getGasFees(request: GasFeeFlowRequest): Promise { + try { + return await this.#getRandomisedGasFees(request); + } catch (error) { + log('Using default flow as fallback due to error', error); + return await this.#getDefaultGasFees(request); + } + } + + async #getDefaultGasFees( + request: GasFeeFlowRequest, + ): Promise { + return new DefaultGasFeeFlow().getGasFees(request); + } + + async #getRandomisedGasFees( + request: GasFeeFlowRequest, + ): Promise { + const { featureFlags, gasFeeControllerData, transactionMeta } = request; + const { gasEstimateType, gasFeeEstimates } = gasFeeControllerData; + + const randomiseGasFeesConfig = getRandomisedGasFeeConfig(featureFlags); + const lastNDigits = randomiseGasFeesConfig[transactionMeta.chainId]; + + let response: GasFeeEstimates; + + switch (gasEstimateType) { + case GAS_ESTIMATE_TYPES.FEE_MARKET: + log('Using fee market estimates', gasFeeEstimates); + response = this.#randomiseFeeMarketEstimates( + gasFeeEstimates, + lastNDigits, + ); + log('Randomised fee market estimates', response); + break; + case GAS_ESTIMATE_TYPES.LEGACY: + log('Using legacy estimates', gasFeeEstimates); + response = this.#randomiseLegacyEstimates( + gasFeeEstimates as LegacyGasPriceEstimate, + lastNDigits, + ); + log('Randomised legacy estimates', response); + break; + case GAS_ESTIMATE_TYPES.ETH_GASPRICE: + log('Using eth_gasPrice estimates', gasFeeEstimates); + response = this.#getRandomisedGasPriceEstimate( + gasFeeEstimates as EthGasPriceEstimate, + lastNDigits, + ); + log('Randomised eth_gasPrice estimates', response); + break; + default: + throw new Error(`Unsupported gas estimate type: ${gasEstimateType}`); + } + + return { + estimates: response, + }; + } + + #randomiseFeeMarketEstimates( + gasFeeEstimates: FeeMarketGasPriceEstimate, + lastNDigits: number, + ): FeeMarketGasFeeEstimates { + const levels = Object.values(GasFeeEstimateLevel).reduce( + (result, level) => ({ + ...result, + [level]: this.#getRandomisedFeeMarketLevel( + gasFeeEstimates, + level, + lastNDigits, + ), + }), + {} as Omit, + ); + + return { + type: GasFeeEstimateType.FeeMarket, + ...levels, + }; + } + + #getRandomisedFeeMarketLevel( + gasFeeEstimates: FeeMarketGasPriceEstimate, + level: GasFeeEstimateLevel, + lastNDigits: number, + ): FeeMarketGasFeeEstimateForLevel { + return { + maxFeePerGas: randomiseDecimalValueAndConvertToHex( + gasFeeEstimates[level].suggestedMaxFeePerGas, + lastNDigits, + ), + maxPriorityFeePerGas: randomiseDecimalValueAndConvertToHex( + gasFeeEstimates[level].suggestedMaxPriorityFeePerGas, + lastNDigits, + ), + }; + } + + #randomiseLegacyEstimates( + gasFeeEstimates: LegacyGasPriceEstimate, + lastNDigits: number, + ): LegacyGasFeeEstimates { + const levels = Object.values(GasFeeEstimateLevel).reduce( + (result, level) => ({ + ...result, + [level]: this.#getRandomisedLegacyLevel( + gasFeeEstimates, + level, + lastNDigits, + ), + }), + {} as Omit, + ); + + return { + type: GasFeeEstimateType.Legacy, + ...levels, + }; + } + + #getRandomisedLegacyLevel( + gasFeeEstimates: LegacyGasPriceEstimate, + level: GasFeeEstimateLevel, + lastNDigits: number, + ): Hex { + return randomiseDecimalValueAndConvertToHex( + gasFeeEstimates[level], + lastNDigits, + ); + } + + #getRandomisedGasPriceEstimate( + gasFeeEstimates: EthGasPriceEstimate, + lastNDigits: number, + ): GasPriceGasFeeEstimates { + return { + type: GasFeeEstimateType.GasPrice, + gasPrice: randomiseDecimalValueAndConvertToHex( + gasFeeEstimates.gasPrice, + lastNDigits, + ), + }; + } +} + +/** + * Returns the randomised gas fee config from the feature flags + * + * @param featureFlags - All feature flags + */ +function getRandomisedGasFeeConfig( + featureFlags: TransactionControllerFeatureFlags, +) { + const randomiseGasFeesConfig = + featureFlags?.[FEATURE_FLAG_RANDOMISE_GAS_FEES]?.config ?? {}; + + return randomiseGasFeesConfig; +} + +/** + * Randomises the least significant digits of a decimal gas fee value and converts it to a hexadecimal Wei value. + * + * This function preserves the more significant digits while randomizing only the least significant ones, + * ensuring that fees remain close to the original estimation while providing fingerprinting protection. + * + * @param gweiDecimalValue - The original gas fee value in Gwei (decimal) + * @param [lastNumberOfDigitsToRandomise] - The number of least significant digits to randomise + * @returns The randomised value converted to Wei in hexadecimal format + * + * @example + * // Randomise last 3 digits of "200000" + * randomiseDecimalValueAndConvertToHex("200000", 3) + * // Decimal output range: 200000 to 200999 + * + * @example + * // Randomise last 5 digits of "200000" + * randomiseDecimalValueAndConvertToHex("200000", 5) + * // Decimal output range: 200000 to 299999 + * + * @example + * // Randomise last 6 digits of "200000" + * randomiseDecimalValueAndConvertToHex("200000", 6) + * // Decimal output range: 200000 to 299999 + * + * @example + * // Randomise last 8 digits of "200000" + * randomiseDecimalValueAndConvertToHex("200000", 8) + * // Decimal output range: 200000 to 299999 + */ +function randomiseDecimalValueAndConvertToHex( + gweiDecimalValue: string | number, + lastNumberOfDigitsToRandomise = 6, +): Hex { + const decimalValue = + typeof gweiDecimalValue === 'string' + ? gweiDecimalValue + : gweiDecimalValue.toString(); + + const decimalLength = decimalValue.length; + + // Determine how many digits to randomise while preserving the first digit + const effectiveDigitsToRandomise = Math.min( + lastNumberOfDigitsToRandomise, + decimalLength - 1, + ); + + const multiplier = 10 ** effectiveDigitsToRandomise; + + // Remove last digits - this keeps the first (decimalLength - effectiveDigitsToRandomise) digits intact + const basePart = Math.floor(Number(decimalValue) / multiplier) * multiplier; + + // Generate random digits + const randomDigits = Math.floor(Math.random() * multiplier); + + // Combine base and random parts + const randomisedDecimal = basePart + randomDigits; + + // Convert to gwei to hex + return gweiDecimalToWeiHex(randomisedDecimal.toString()); +} diff --git a/packages/transaction-controller/src/helpers/GasFeePoller.test.ts b/packages/transaction-controller/src/helpers/GasFeePoller.test.ts index 348921c38c9..e1dbb2faa12 100644 --- a/packages/transaction-controller/src/helpers/GasFeePoller.test.ts +++ b/packages/transaction-controller/src/helpers/GasFeePoller.test.ts @@ -3,6 +3,7 @@ import type { Hex } from '@metamask/utils'; import { GasFeePoller, updateTransactionGasFees } from './GasFeePoller'; import { flushPromises } from '../../../../tests/helpers'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { GasFeeFlowResponse, Layer1GasFeeFlow } from '../types'; import { GasFeeEstimateLevel, @@ -14,8 +15,13 @@ import { type GasFeeEstimates, type TransactionMeta, } from '../types'; +import { + getFeatureFlags, + type TransactionControllerFeatureFlags, +} from '../utils/feature-flags'; import { getTransactionLayer1GasFee } from '../utils/layer1-gas-fee-flow'; +jest.mock('../utils/feature-flags'); jest.mock('../utils/layer1-gas-fee-flow', () => ({ getTransactionLayer1GasFee: jest.fn(), })); @@ -77,11 +83,17 @@ describe('GasFeePoller', () => { const layer1GasFeeFlowsMock: jest.Mocked = []; const getGasFeeControllerEstimatesMock = jest.fn(); const findNetworkClientIdByChainIdMock = jest.fn(); + const messengerMock = jest.fn() as unknown as TransactionControllerMessenger; + const getFeatureFlagsMock = jest.mocked(getFeatureFlags); beforeEach(() => { jest.clearAllTimers(); jest.clearAllMocks(); + getFeatureFlagsMock.mockReturnValue( + {} as unknown as TransactionControllerFeatureFlags, + ); + gasFeeFlowMock = createGasFeeFlowMock(); gasFeeFlowMock.matchesTransaction.mockReturnValue(true); gasFeeFlowMock.getGasFees.mockResolvedValue(GAS_FEE_FLOW_RESPONSE_MOCK); @@ -97,6 +109,7 @@ describe('GasFeePoller', () => { getGasFeeControllerEstimates: getGasFeeControllerEstimatesMock, getTransactions: getTransactionsMock, layer1GasFeeFlows: layer1GasFeeFlowsMock, + messenger: messengerMock, onStateChange: (listener: () => void) => { triggerOnStateChange = listener; }, @@ -125,6 +138,13 @@ describe('GasFeePoller', () => { }); it('calls gas fee flow', async () => { + const mockFeatureFlags = { + test: { + config: {}, + }, + } as unknown as TransactionControllerFeatureFlags; + + getFeatureFlagsMock.mockReturnValue(mockFeatureFlags); getGasFeeControllerEstimatesMock.mockResolvedValue({}); new GasFeePoller(constructorOptions); @@ -135,6 +155,7 @@ describe('GasFeePoller', () => { expect(gasFeeFlowMock.getGasFees).toHaveBeenCalledTimes(1); expect(gasFeeFlowMock.getGasFees).toHaveBeenCalledWith({ ethQuery: expect.any(Object), + featureFlags: mockFeatureFlags, gasFeeControllerData: {}, transactionMeta: TRANSACTION_META_MOCK, }); diff --git a/packages/transaction-controller/src/helpers/GasFeePoller.ts b/packages/transaction-controller/src/helpers/GasFeePoller.ts index 87c3d930821..4186a00e6c4 100644 --- a/packages/transaction-controller/src/helpers/GasFeePoller.ts +++ b/packages/transaction-controller/src/helpers/GasFeePoller.ts @@ -11,6 +11,7 @@ import { createModuleLogger } from '@metamask/utils'; import EventEmitter from 'events'; import { projectLogger } from '../logger'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { GasFeeEstimates, GasFeeFlow, @@ -27,6 +28,7 @@ import { TransactionStatus, TransactionEnvelopeType, } from '../types'; +import { getFeatureFlags } from '../utils/feature-flags'; import { getGasFeeFlow } from '../utils/gas-flow'; import { getTransactionLayer1GasFee } from '../utils/layer1-gas-fee-flow'; @@ -56,6 +58,8 @@ export class GasFeePoller { readonly #layer1GasFeeFlows: Layer1GasFeeFlow[]; + readonly #messenger: TransactionControllerMessenger; + #timeout: ReturnType | undefined; #running = false; @@ -70,6 +74,7 @@ export class GasFeePoller { * @param options.getProvider - Callback to obtain a provider instance. * @param options.getTransactions - Callback to obtain the transaction data. * @param options.layer1GasFeeFlows - The layer 1 gas fee flows to use to obtain suitable layer 1 gas fees. + * @param options.messenger - The TransactionControllerMessenger instance. * @param options.onStateChange - Callback to register a listener for controller state changes. */ constructor({ @@ -79,6 +84,7 @@ export class GasFeePoller { getProvider, getTransactions, layer1GasFeeFlows, + messenger, onStateChange, }: { findNetworkClientIdByChainId: (chainId: Hex) => NetworkClientId | undefined; @@ -89,6 +95,7 @@ export class GasFeePoller { getProvider: (networkClientId: NetworkClientId) => Provider; getTransactions: () => TransactionMeta[]; layer1GasFeeFlows: Layer1GasFeeFlow[]; + messenger: TransactionControllerMessenger; onStateChange: (listener: () => void) => void; }) { this.#findNetworkClientIdByChainId = findNetworkClientIdByChainId; @@ -97,6 +104,7 @@ export class GasFeePoller { this.#getGasFeeControllerEstimates = getGasFeeControllerEstimates; this.#getProvider = getProvider; this.#getTransactions = getTransactions; + this.#messenger = messenger; onStateChange(() => { const unapprovedTransactions = this.#getUnapprovedTransactions(); @@ -206,8 +214,13 @@ export class GasFeePoller { > { const { networkClientId } = transactionMeta; + const featureFlags = getFeatureFlags(this.#messenger); const ethQuery = new EthQuery(this.#getProvider(networkClientId)); - const gasFeeFlow = getGasFeeFlow(transactionMeta, this.#gasFeeFlows); + const gasFeeFlow = getGasFeeFlow( + transactionMeta, + this.#gasFeeFlows, + featureFlags, + ); if (gasFeeFlow) { log( @@ -219,6 +232,7 @@ export class GasFeePoller { const request: GasFeeFlowRequest = { ethQuery, + featureFlags, gasFeeControllerData, transactionMeta, }; diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 946e80eb973..612921544a5 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -6,6 +6,8 @@ import type { NetworkClientId, Provider } from '@metamask/network-controller'; import type { Hex, Json } from '@metamask/utils'; import type { Operation } from 'fast-json-patch'; +import type { TransactionControllerFeatureFlags } from './utils/feature-flags'; + /** * Given a record, ensures that each property matches the `Json` type. */ @@ -1210,6 +1212,9 @@ export type GasFeeFlowRequest = { /** An EthQuery instance to enable queries to the associated RPC provider. */ ethQuery: EthQuery; + /** The feature flags for the transaction controller. */ + featureFlags: TransactionControllerFeatureFlags; + /** Gas fee controller data matching the chain ID of the transaction. */ gasFeeControllerData: GasFeeState; @@ -1231,7 +1236,10 @@ export type GasFeeFlow = { * @param transactionMeta - The transaction metadata. * @returns Whether the gas fee flow supports the transaction. */ - matchesTransaction(transactionMeta: TransactionMeta): boolean; + matchesTransaction( + transactionMeta: TransactionMeta, + featureFlags: TransactionControllerFeatureFlags, + ): boolean; /** * Get gas fee estimates for a specific transaction. diff --git a/packages/transaction-controller/src/utils/feature-flags.ts b/packages/transaction-controller/src/utils/feature-flags.ts index 9be6d52ec2d..1f7ccceeb50 100644 --- a/packages/transaction-controller/src/utils/feature-flags.ts +++ b/packages/transaction-controller/src/utils/feature-flags.ts @@ -6,6 +6,8 @@ import type { TransactionControllerMessenger } from '../TransactionController'; export const FEATURE_FLAG_TRANSACTIONS = 'confirmations_transactions'; export const FEATURE_FLAG_EIP_7702 = 'confirmations_eip_7702'; +export const FEATURE_FLAG_RANDOMISE_GAS_FEES = + 'confirmations-randomise-gas-fees'; const DEFAULT_BATCH_SIZE_LIMIT = 10; const DEFAULT_ACCELERATED_POLLING_COUNT_MAX = 10; @@ -64,6 +66,15 @@ export type TransactionControllerFeatureFlags = { defaultIntervalMs?: number; }; }; + + [FEATURE_FLAG_RANDOMISE_GAS_FEES]?: { + /** + * Config for randomizing gas fees. + * Keyed by chain ID. + * Value is the number of digits to randomise. + */ + config?: Record; + }; }; const log = createModuleLogger(projectLogger, 'feature-flags'); @@ -180,7 +191,7 @@ export function getAcceleratedPollingParams( * @param messenger - The messenger instance. * @returns The feature flags. */ -function getFeatureFlags( +export function getFeatureFlags( messenger: TransactionControllerMessenger, ): TransactionControllerFeatureFlags { const featureFlags = messenger.call( diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index 1aaf7aa8fa8..0fe1c20ac2c 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -12,6 +12,7 @@ import type { import type { Hex } from '@metamask/utils'; import { add0x, createModuleLogger } from '@metamask/utils'; +import type { TransactionControllerFeatureFlags } from './feature-flags'; import { getGasFeeFlow } from './gas-flow'; import { SWAP_TRANSACTION_TYPES } from './swaps'; import { projectLogger } from '../logger'; @@ -27,6 +28,7 @@ import { GasFeeEstimateType, UserFeeLevel } from '../types'; export type UpdateGasFeesRequest = { eip1559: boolean; ethQuery: EthQuery; + featureFlags: TransactionControllerFeatureFlags; gasFeeFlows: GasFeeFlow[]; getGasFeeEstimates: ( options: FetchGasFeeEstimateOptions, @@ -326,8 +328,14 @@ function updateDefaultGasEstimates(txMeta: TransactionMeta) { async function getSuggestedGasFees( request: UpdateGasFeesRequest, ): Promise { - const { eip1559, ethQuery, gasFeeFlows, getGasFeeEstimates, txMeta } = - request; + const { + eip1559, + ethQuery, + featureFlags, + gasFeeFlows, + getGasFeeEstimates, + txMeta, + } = request; const { networkClientId } = txMeta; @@ -340,13 +348,18 @@ async function getSuggestedGasFees( return {}; } - const gasFeeFlow = getGasFeeFlow(txMeta, gasFeeFlows) as GasFeeFlow; + const gasFeeFlow = getGasFeeFlow( + txMeta, + gasFeeFlows, + featureFlags, + ) as GasFeeFlow; try { const gasFeeControllerData = await getGasFeeEstimates({ networkClientId }); const response = await gasFeeFlow.getGasFees({ ethQuery, + featureFlags, gasFeeControllerData, transactionMeta: txMeta, }); diff --git a/packages/transaction-controller/src/utils/gas-flow.test.ts b/packages/transaction-controller/src/utils/gas-flow.test.ts index 2d8e7f4e31a..4b4b5ae2a99 100644 --- a/packages/transaction-controller/src/utils/gas-flow.test.ts +++ b/packages/transaction-controller/src/utils/gas-flow.test.ts @@ -1,5 +1,6 @@ import type { GasFeeEstimates as GasFeeControllerEstimates } from '@metamask/gas-fee-controller'; +import type { TransactionControllerFeatureFlags } from './feature-flags'; import { getGasFeeFlow, mergeGasFeeEstimates } from './gas-flow'; import type { FeeMarketGasFeeEstimates, @@ -86,6 +87,12 @@ function createGasFeeFlowMock(): jest.Mocked { describe('gas-flow', () => { describe('getGasFeeFlow', () => { + const featureFlags = { + test: { + config: {}, + }, + } as unknown as TransactionControllerFeatureFlags; + it('returns undefined if no gas fee flow matches transaction', () => { const gasFeeFlow1 = createGasFeeFlowMock(); const gasFeeFlow2 = createGasFeeFlowMock(); @@ -94,7 +101,11 @@ describe('gas-flow', () => { gasFeeFlow2.matchesTransaction.mockReturnValue(false); expect( - getGasFeeFlow(TRANSACTION_META_MOCK, [gasFeeFlow1, gasFeeFlow2]), + getGasFeeFlow( + TRANSACTION_META_MOCK, + [gasFeeFlow1, gasFeeFlow2], + featureFlags, + ), ).toBeUndefined(); }); @@ -106,7 +117,11 @@ describe('gas-flow', () => { gasFeeFlow2.matchesTransaction.mockReturnValue(true); expect( - getGasFeeFlow(TRANSACTION_META_MOCK, [gasFeeFlow1, gasFeeFlow2]), + getGasFeeFlow( + TRANSACTION_META_MOCK, + [gasFeeFlow1, gasFeeFlow2], + featureFlags, + ), ).toBe(gasFeeFlow2); }); }); diff --git a/packages/transaction-controller/src/utils/gas-flow.ts b/packages/transaction-controller/src/utils/gas-flow.ts index 94cf4ed0b7f..822b169ffde 100644 --- a/packages/transaction-controller/src/utils/gas-flow.ts +++ b/packages/transaction-controller/src/utils/gas-flow.ts @@ -7,6 +7,7 @@ import type { } from '@metamask/gas-fee-controller'; import { type GasFeeState } from '@metamask/gas-fee-controller'; +import type { TransactionControllerFeatureFlags } from './feature-flags'; import type { FeeMarketGasFeeEstimates, GasPriceGasFeeEstimates, @@ -36,14 +37,16 @@ type MergeGasFeeEstimatesRequest = { * * @param transactionMeta - The transaction metadata to find a gas fee flow for. * @param gasFeeFlows - The gas fee flows to search. + * @param featureFlags - All feature flags * @returns The first gas fee flow that matches the transaction, or undefined if none match. */ export function getGasFeeFlow( transactionMeta: TransactionMeta, gasFeeFlows: GasFeeFlow[], + featureFlags: TransactionControllerFeatureFlags, ): GasFeeFlow | undefined { return gasFeeFlows.find((gasFeeFlow) => - gasFeeFlow.matchesTransaction(transactionMeta), + gasFeeFlow.matchesTransaction(transactionMeta, featureFlags), ); } From 730ed0f99da43a24c6e784afead4820b6ac01d16 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 12:35:50 +0100 Subject: [PATCH 02/24] Remove unusued prop --- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index d8284cb66e8..6a1671f6125 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -5,7 +5,6 @@ import type { } from '@metamask/gas-fee-controller'; import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; import { createModuleLogger, type Hex } from '@metamask/utils'; -import BN from 'bn.js'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; import { projectLogger } from '../logger'; From 3ef027f2d5c51feddcd9ba636ea47990057f118f Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 12:49:47 +0100 Subject: [PATCH 03/24] Fix lint --- packages/transaction-controller/CHANGELOG.md | 5 +++++ .../src/gas-flows/DefaultGasFeeFlow.test.ts | 2 +- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts | 4 ++-- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.ts | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/transaction-controller/CHANGELOG.md b/packages/transaction-controller/CHANGELOG.md index 394e2df4d4f..0d8ca7cae26 100644 --- a/packages/transaction-controller/CHANGELOG.md +++ b/packages/transaction-controller/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Adds `RandomisedEstimationsGasFeeFlow` to gas fee flows in `TransactionController` ([#5511](https://github.com/MetaMask/core/pull/5511)) + - Added flow only will be activated if chainId is defined in feature flags. + ### Fixed - Fix simulation of type-4 transactions ([#5552](https://github.com/MetaMask/core/pull/5552)) diff --git a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts index 09076c0c60f..849b2dd673c 100644 --- a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts @@ -8,7 +8,6 @@ import type { import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; -import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; import type { FeeMarketGasFeeEstimates, GasPriceGasFeeEstimates, @@ -16,6 +15,7 @@ import type { TransactionMeta, } from '../types'; import { GasFeeEstimateType, TransactionStatus } from '../types'; +import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; const ETH_QUERY_MOCK = {} as EthQuery; diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 988b93cdaa5..91c1d5e5b5a 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -335,7 +335,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { expect(DefaultGasFeeFlow.prototype.getGasFees).toHaveBeenCalledWith( request, ); - expect(result.estimates).toEqual(DEFAULT_FEE_MARKET_RESPONSE); + expect(result.estimates).toStrictEqual(DEFAULT_FEE_MARKET_RESPONSE); }); it('should throw an error for unsupported gas estimate types', async () => { @@ -359,7 +359,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { expect(DefaultGasFeeFlow.prototype.getGasFees).toHaveBeenCalledWith( request, ); - expect(result.estimates).toEqual(DEFAULT_GAS_PRICE_RESPONSE); + expect(result.estimates).toStrictEqual(DEFAULT_GAS_PRICE_RESPONSE); spy.mockRestore(); }); }); diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 6a1671f6125..51bcfb470a5 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -194,12 +194,12 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { /** * Returns the randomised gas fee config from the feature flags - * + * * @param featureFlags - All feature flags */ function getRandomisedGasFeeConfig( featureFlags: TransactionControllerFeatureFlags, -) { +): Record { const randomiseGasFeesConfig = featureFlags?.[FEATURE_FLAG_RANDOMISE_GAS_FEES]?.config ?? {}; From 67f9d2737bad2ac30a8904670678ad2eeb008ade Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 12:57:05 +0100 Subject: [PATCH 04/24] Fix config --- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 51bcfb470a5..59034c97f32 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -196,6 +196,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { * Returns the randomised gas fee config from the feature flags * * @param featureFlags - All feature flags + * @returns The randomised gas fee config */ function getRandomisedGasFeeConfig( featureFlags: TransactionControllerFeatureFlags, From 625ee82b4b1d2b8d833a7cca7fd0fd3ea55b195c Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 13:03:59 +0100 Subject: [PATCH 05/24] Add constants --- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 59034c97f32..d26063ab77d 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -29,6 +29,9 @@ const log = createModuleLogger( 'randomised-estimation-gas-fee-flow', ); +const PRESERVE_NUMBER_OF_DIGITS = 1; +const DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE = 4; + /** * Implementation of a gas fee flow that randomises the last digits of gas fee estimations */ @@ -239,7 +242,7 @@ function getRandomisedGasFeeConfig( */ function randomiseDecimalValueAndConvertToHex( gweiDecimalValue: string | number, - lastNumberOfDigitsToRandomise = 6, + lastNumberOfDigitsToRandomise = DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE, ): Hex { const decimalValue = typeof gweiDecimalValue === 'string' @@ -248,10 +251,10 @@ function randomiseDecimalValueAndConvertToHex( const decimalLength = decimalValue.length; - // Determine how many digits to randomise while preserving the first digit + // Determine how many digits to randomise while preserving the PRESERVE_NUMBER_OF_DIGITS const effectiveDigitsToRandomise = Math.min( lastNumberOfDigitsToRandomise, - decimalLength - 1, + decimalLength - PRESERVE_NUMBER_OF_DIGITS, ); const multiplier = 10 ** effectiveDigitsToRandomise; From fcb7d602dd1c5a586ae55ce06c39ca3163a3b641 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 14:04:22 +0100 Subject: [PATCH 06/24] Make randomisation in wei unit --- .../RandomisedEstimationsGasFeeFlow.ts | 60 +++++++++---------- .../src/utils/gas-fees.ts | 21 +++++++ 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index d26063ab77d..6fa7c9dfcbf 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -22,14 +22,14 @@ import type { import { GasFeeEstimateLevel, GasFeeEstimateType } from '../types'; import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; import { FEATURE_FLAG_RANDOMISE_GAS_FEES } from '../utils/feature-flags'; -import { gweiDecimalToWeiHex } from '../utils/gas-fees'; +import { gweiDecimalToWeiDecimal } from '../utils/gas-fees'; const log = createModuleLogger( projectLogger, 'randomised-estimation-gas-fee-flow', ); -const PRESERVE_NUMBER_OF_DIGITS = 1; +const PRESERVE_NUMBER_OF_DIGITS = 2; const DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE = 4; /** @@ -137,11 +137,11 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { lastNDigits: number, ): FeeMarketGasFeeEstimateForLevel { return { - maxFeePerGas: randomiseDecimalValueAndConvertToHex( + maxFeePerGas: randomiseDecimalGWEIAndConvertToHex( gasFeeEstimates[level].suggestedMaxFeePerGas, lastNDigits, ), - maxPriorityFeePerGas: randomiseDecimalValueAndConvertToHex( + maxPriorityFeePerGas: randomiseDecimalGWEIAndConvertToHex( gasFeeEstimates[level].suggestedMaxPriorityFeePerGas, lastNDigits, ), @@ -175,7 +175,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { level: GasFeeEstimateLevel, lastNDigits: number, ): Hex { - return randomiseDecimalValueAndConvertToHex( + return randomiseDecimalGWEIAndConvertToHex( gasFeeEstimates[level], lastNDigits, ); @@ -187,7 +187,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { ): GasPriceGasFeeEstimates { return { type: GasFeeEstimateType.GasPrice, - gasPrice: randomiseDecimalValueAndConvertToHex( + gasPrice: randomiseDecimalGWEIAndConvertToHex( gasFeeEstimates.gasPrice, lastNDigits, ), @@ -214,42 +214,39 @@ function getRandomisedGasFeeConfig( * Randomises the least significant digits of a decimal gas fee value and converts it to a hexadecimal Wei value. * * This function preserves the more significant digits while randomizing only the least significant ones, - * ensuring that fees remain close to the original estimation while providing fingerprinting protection. + * ensuring that fees remain close to the original estimation while providing randomisation. + * The randomisation is performed in Wei units for more precision. * * @param gweiDecimalValue - The original gas fee value in Gwei (decimal) * @param [lastNumberOfDigitsToRandomise] - The number of least significant digits to randomise * @returns The randomised value converted to Wei in hexadecimal format * * @example - * // Randomise last 3 digits of "200000" - * randomiseDecimalValueAndConvertToHex("200000", 3) - * // Decimal output range: 200000 to 200999 + * // Randomise last 3 digits of "5" Gwei (5000000000 Wei) + * randomiseDecimalGWEIAndConvertToHex("5", 3) + * // Decimal output range: 5000000000 to 5000000999 Wei + * // Hex output range: 0x12a05f200 to 0x12a05f3e7 * * @example - * // Randomise last 5 digits of "200000" - * randomiseDecimalValueAndConvertToHex("200000", 5) - * // Decimal output range: 200000 to 299999 + * // Randomise last 6 digits of "10.5" Gwei (10500000000 Wei) + * randomiseDecimalGWEIAndConvertToHex("10.5", 6) + * // Decimal output range: 10500000000 to 10500999999 Wei + * // Hex output range: 0x27312d600 to 0x27313f9cf * * @example - * // Randomise last 6 digits of "200000" - * randomiseDecimalValueAndConvertToHex("200000", 6) - * // Decimal output range: 200000 to 299999 - * - * @example - * // Randomise last 8 digits of "200000" - * randomiseDecimalValueAndConvertToHex("200000", 8) - * // Decimal output range: 200000 to 299999 + * // Randomise last 9 digits of "42" Gwei (42000000000 Wei) + * randomiseDecimalGWEIAndConvertToHex("42", 9) + * // Decimal output range: 42000000000 to 42999999999 Wei + * // Hex output range: 0x9c7652400 to 0x9fffff9ff */ -function randomiseDecimalValueAndConvertToHex( +function randomiseDecimalGWEIAndConvertToHex( gweiDecimalValue: string | number, lastNumberOfDigitsToRandomise = DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE, ): Hex { - const decimalValue = - typeof gweiDecimalValue === 'string' - ? gweiDecimalValue - : gweiDecimalValue.toString(); + // First convert GWEI to WEI decimal + const weiDecimalValue = gweiDecimalToWeiDecimal(gweiDecimalValue); - const decimalLength = decimalValue.length; + const decimalLength = weiDecimalValue.length; // Determine how many digits to randomise while preserving the PRESERVE_NUMBER_OF_DIGITS const effectiveDigitsToRandomise = Math.min( @@ -260,14 +257,15 @@ function randomiseDecimalValueAndConvertToHex( const multiplier = 10 ** effectiveDigitsToRandomise; // Remove last digits - this keeps the first (decimalLength - effectiveDigitsToRandomise) digits intact - const basePart = Math.floor(Number(decimalValue) / multiplier) * multiplier; + const basePart = + Math.floor(Number(weiDecimalValue) / multiplier) * multiplier; // Generate random digits const randomDigits = Math.floor(Math.random() * multiplier); // Combine base and random parts - const randomisedDecimal = basePart + randomDigits; + const randomisedWeiDecimal = basePart + randomDigits; - // Convert to gwei to hex - return gweiDecimalToWeiHex(randomisedDecimal.toString()); + // Convert wei decimal to hex + return `0x${randomisedWeiDecimal.toString(16)}` as Hex; } diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index 0fe1c20ac2c..33a5747a04a 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -114,6 +114,27 @@ export function gweiDecimalToWeiHex(value: string) { return toHex(gweiDecToWEIBN(value)); } +/** + * Converts a value from Gwei decimal representation to Wei decimal representation + * + * @param gweiDecimal - The value in Gwei as a string or number + * @returns The value in Wei as a string + * + * @example + * // Convert 1.5 Gwei to Wei + * gweiDecimalToWeiDecimal("1.5") + * // Returns "1500000000" + */ +export function gweiDecimalToWeiDecimal(gweiDecimal: string | number): string { + const gwei = + typeof gweiDecimal === 'string' ? gweiDecimal : gweiDecimal.toString(); + + // 1 Gwei = 10^9 Wei + const weiDecimal = Number(gwei) * 1_000_000_000; + + return weiDecimal.toString(); +} + /** * Determine the maxFeePerGas value for the transaction. * From ca7884abab929a7341553cd3fb371a15bdf86b42 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 14:11:10 +0100 Subject: [PATCH 07/24] Remove unnecessary comments --- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.ts | 2 -- packages/transaction-controller/src/utils/gas-fees.ts | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 6fa7c9dfcbf..4a73e64706e 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -243,9 +243,7 @@ function randomiseDecimalGWEIAndConvertToHex( gweiDecimalValue: string | number, lastNumberOfDigitsToRandomise = DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE, ): Hex { - // First convert GWEI to WEI decimal const weiDecimalValue = gweiDecimalToWeiDecimal(gweiDecimalValue); - const decimalLength = weiDecimalValue.length; // Determine how many digits to randomise while preserving the PRESERVE_NUMBER_OF_DIGITS diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index 33a5747a04a..109d006eae0 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -129,8 +129,7 @@ export function gweiDecimalToWeiDecimal(gweiDecimal: string | number): string { const gwei = typeof gweiDecimal === 'string' ? gweiDecimal : gweiDecimal.toString(); - // 1 Gwei = 10^9 Wei - const weiDecimal = Number(gwei) * 1_000_000_000; + const weiDecimal = Number(gwei) * 1e9; return weiDecimal.toString(); } From 1c1901b0cee43ab84ccc6015fc4df8ed9f272f2b Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 14:17:15 +0100 Subject: [PATCH 08/24] Remove unnecessary comment --- .../transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts index e5d9c96f84f..6b351cb51c0 100644 --- a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts @@ -56,8 +56,6 @@ export class DefaultGasFeeFlow implements GasFeeFlow { ); break; default: - // TODO: Either fix this lint violation or explain why it's necessary to ignore. - throw new Error(`Unsupported gas estimate type: ${gasEstimateType}`); } From 9feef1b87cafcf8393077db33513fcbbafb62e4e Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 14:33:23 +0100 Subject: [PATCH 09/24] Remove comment --- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 91c1d5e5b5a..3739cc91967 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -143,8 +143,6 @@ describe('RandomisedEstimationsGasFeeFlow', () => { }); }); - // ... existing code ... - describe('getGasFees', () => { it('randomises fee market estimates for chain IDs in the feature flag config', async () => { const flow = new RandomisedEstimationsGasFeeFlow(); From f34191dd388fd25d9f3d6690048dfdecd1b8fdef Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 14:50:20 +0100 Subject: [PATCH 10/24] Add unit tests for gweiDecimalToWeiDecimal --- .../src/utils/gas-fees.test.ts | 28 ++++++++++++++++++- .../src/utils/gas-fees.ts | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/transaction-controller/src/utils/gas-fees.test.ts b/packages/transaction-controller/src/utils/gas-fees.test.ts index fbef41ffce4..69f73f6d0b7 100644 --- a/packages/transaction-controller/src/utils/gas-fees.test.ts +++ b/packages/transaction-controller/src/utils/gas-fees.test.ts @@ -1,7 +1,7 @@ import { ORIGIN_METAMASK, query } from '@metamask/controller-utils'; import type { UpdateGasFeesRequest } from './gas-fees'; -import { updateGasFees } from './gas-fees'; +import { gweiDecimalToWeiDecimal, updateGasFees } from './gas-fees'; import type { GasFeeFlow, GasFeeFlowResponse } from '../types'; import { GasFeeEstimateType, TransactionType, UserFeeLevel } from '../types'; @@ -550,3 +550,29 @@ describe('gas-fees', () => { }); }); }); + +describe('gweiDecimalToWeiDecimal', () => { + it('converts string gwei decimal to wei decimal', () => { + expect(gweiDecimalToWeiDecimal('1')).toBe('1000000000'); + expect(gweiDecimalToWeiDecimal('1.5')).toBe('1500000000'); + expect(gweiDecimalToWeiDecimal('0.1')).toBe('100000000'); + expect(gweiDecimalToWeiDecimal('123.456')).toBe('123456000000'); + }); + + it('converts number gwei decimal to wei decimal', () => { + expect(gweiDecimalToWeiDecimal(1)).toBe('1000000000'); + expect(gweiDecimalToWeiDecimal(1.5)).toBe('1500000000'); + expect(gweiDecimalToWeiDecimal(0.1)).toBe('100000000'); + expect(gweiDecimalToWeiDecimal(123.456)).toBe('123456000000'); + }); + + it('handles zero values', () => { + expect(gweiDecimalToWeiDecimal('0')).toBe('0'); + expect(gweiDecimalToWeiDecimal(0)).toBe('0'); + }); + + it('handles very large values', () => { + expect(gweiDecimalToWeiDecimal('1000000')).toBe('1000000000000000'); + expect(gweiDecimalToWeiDecimal(1000000)).toBe('1000000000000000'); + }); +}); diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index 109d006eae0..4eb731d9fbc 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -127,7 +127,7 @@ export function gweiDecimalToWeiHex(value: string) { */ export function gweiDecimalToWeiDecimal(gweiDecimal: string | number): string { const gwei = - typeof gweiDecimal === 'string' ? gweiDecimal : gweiDecimal.toString(); + typeof gweiDecimal === 'string' ? gweiDecimal : String(gweiDecimal); const weiDecimal = Number(gwei) * 1e9; From 488c21e20a82278d3e0886d4c39c9af66b6b2ac0 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 15:28:45 +0100 Subject: [PATCH 11/24] Update unit tests for randomiseDecimalGWEIAndConvertToHex --- .../RandomisedEstimationsGasFeeFlow.test.ts | 117 +++++++++++++++++- .../RandomisedEstimationsGasFeeFlow.ts | 31 +++-- 2 files changed, 139 insertions(+), 9 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 3739cc91967..42e03237075 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -4,7 +4,10 @@ import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; import type { GasFeeState } from '@metamask/gas-fee-controller'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; -import { RandomisedEstimationsGasFeeFlow } from './RandomisedEstimationsGasFeeFlow'; +import { + RandomisedEstimationsGasFeeFlow, + randomiseDecimalGWEIAndConvertToHex, +} from './RandomisedEstimationsGasFeeFlow'; import type { FeeMarketGasFeeEstimates, GasPriceGasFeeEstimates, @@ -362,3 +365,115 @@ describe('RandomisedEstimationsGasFeeFlow', () => { }); }); }); + +describe('randomiseDecimalGWEIAndConvertToHex', () => { + beforeEach(() => { + jest.spyOn(global.Math, 'random').mockReturnValue(0.5); + }); + + afterEach(() => { + jest.spyOn(global.Math, 'random').mockRestore(); + }); + + it('randomizes the last digits while preserving the significant digits', () => { + const result = randomiseDecimalGWEIAndConvertToHex('5', 3); + + const resultWei = parseInt(result.slice(2), 16); + const resultGwei = resultWei / 1e9; + + // With Math.random = 0.5, we expect the last 3 digits to be around 500 + // The expected value should be 5.0000005 (not 5.0005) + expect(resultGwei).toBeCloseTo(5.0000005, 6); + + // The base part should be exactly 5.000 Gwei + const basePart = (Math.floor(resultWei / 1000) * 1000) / 1e9; + expect(basePart).toEqual(5); + }); + + it('ensures randomized value is never below original value', () => { + // Test with Math.random = 0 (lowest possible random value) + jest.spyOn(global.Math, 'random').mockReturnValue(0); + + // Test with a value that has non-zero ending digits + const result = randomiseDecimalGWEIAndConvertToHex('5.000500123', 3); + const resultWei = parseInt(result.slice(2), 16); + + // Original value in Wei + const originalWei = 5000500123; + + // With Math.random = 0, result should exactly equal original value + expect(resultWei).toBe(originalWei); + }); + + it('randomizes up to but not exceeding the specified number of digits', () => { + // Set Math.random to return almost 1 + jest.spyOn(global.Math, 'random').mockReturnValue(0.999); + + const result = randomiseDecimalGWEIAndConvertToHex('5', 3); + const resultWei = parseInt(result.slice(2), 16); + + const baseWei = 5 * 1e9; + + // With 3 digits and Math.random almost 1, we expect the last 3 digits to be close to 999 + expect(resultWei).toBeGreaterThanOrEqual(baseWei); + expect(resultWei).toBeLessThanOrEqual(baseWei + 999); + expect(resultWei).toBeCloseTo(baseWei + 999, -1); + }); + + it('handles values with more digits than requested to randomize', () => { + const result = randomiseDecimalGWEIAndConvertToHex('1.23456789', 2); + const resultWei = parseInt(result.slice(2), 16); + + // Base should be 1.234567 Gwei in Wei + const basePart = Math.floor(resultWei / 100) * 100; + expect(basePart).toBe(1234567800); + + // Original ending digits: 89 + const originalEndingDigits = 89; + + // Randomized part should be in range [89-99] + const randomizedPart = resultWei - basePart; + expect(randomizedPart).toBeGreaterThanOrEqual(originalEndingDigits); + expect(randomizedPart).toBeLessThanOrEqual(99); + }); + + it('respects the PRESERVE_NUMBER_OF_DIGITS constant', () => { + const result = randomiseDecimalGWEIAndConvertToHex('0.00001', 4); + const resultWei = parseInt(result.slice(2), 16); + + // Original value is 10000 Wei + // With PRESERVE_NUMBER_OF_DIGITS = 2, we can randomize at most 3 digits + // Base should be 10000 - (10000 % 1000) = 10000 + const basePart = Math.floor(resultWei / 1000) * 1000; + expect(basePart).toBe(10000); + + // Result should stay within allowed range + expect(resultWei).toBeGreaterThanOrEqual(10000); + expect(resultWei).toBeLessThanOrEqual(10999); + }); + + it('handles edge case with zero', () => { + // For "0" input, the result should still be 0 + // This is because 0 has no "ending digits" to randomize + // The implementation will still start from 0 and only randomize upward + + const result = randomiseDecimalGWEIAndConvertToHex('0', 3); + const resultWei = parseInt(result.slice(2), 16); + + expect(resultWei).toBeGreaterThanOrEqual(0); + + if (resultWei === 0) { + // If it returns 0, that's valid + expect(resultWei).toBe(0); + } else { + // If it returns a randomized value, it should be in the expected range + expect(resultWei).toBeLessThanOrEqual(999); + } + }); + + it('handles different number formats correctly', () => { + const resultFromNumber = randomiseDecimalGWEIAndConvertToHex(5, 3); + const resultFromString = randomiseDecimalGWEIAndConvertToHex('5', 3); + expect(resultFromNumber).toEqual(resultFromString); + }); +}); diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 4a73e64706e..793b55235d2 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -218,7 +218,7 @@ function getRandomisedGasFeeConfig( * The randomisation is performed in Wei units for more precision. * * @param gweiDecimalValue - The original gas fee value in Gwei (decimal) - * @param [lastNumberOfDigitsToRandomise] - The number of least significant digits to randomise + * @param [numberOfDigitsToRandomizeAtTheEnd] - The number of least significant digits to randomise * @returns The randomised value converted to Wei in hexadecimal format * * @example @@ -234,35 +234,50 @@ function getRandomisedGasFeeConfig( * // Hex output range: 0x27312d600 to 0x27313f9cf * * @example + * // Randomise last 3 digits of value with existing decimals (5000500123 Wei) + * randomiseDecimalGWEIAndConvertToHex("5.000500123", 3) + * // Base part: 5000500000 Wei + * // Original ending digits: 123 + * // Random ending digits: 123-999 + * // Decimal output range: 5000500123 to 5000500999 Wei + * // Hex output range: 0x12a05f247b to 0x12a05f3e7 + * + * @example * // Randomise last 9 digits of "42" Gwei (42000000000 Wei) * randomiseDecimalGWEIAndConvertToHex("42", 9) * // Decimal output range: 42000000000 to 42999999999 Wei * // Hex output range: 0x9c7652400 to 0x9fffff9ff */ -function randomiseDecimalGWEIAndConvertToHex( +export function randomiseDecimalGWEIAndConvertToHex( gweiDecimalValue: string | number, - lastNumberOfDigitsToRandomise = DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE, + numberOfDigitsToRandomizeAtTheEnd = DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE, ): Hex { const weiDecimalValue = gweiDecimalToWeiDecimal(gweiDecimalValue); const decimalLength = weiDecimalValue.length; // Determine how many digits to randomise while preserving the PRESERVE_NUMBER_OF_DIGITS const effectiveDigitsToRandomise = Math.min( - lastNumberOfDigitsToRandomise, + numberOfDigitsToRandomizeAtTheEnd, decimalLength - PRESERVE_NUMBER_OF_DIGITS, ); const multiplier = 10 ** effectiveDigitsToRandomise; - // Remove last digits - this keeps the first (decimalLength - effectiveDigitsToRandomise) digits intact + // Keep the original value up to the digits we want to preserve const basePart = Math.floor(Number(weiDecimalValue) / multiplier) * multiplier; - // Generate random digits - const randomDigits = Math.floor(Math.random() * multiplier); + // Get the original ending digits + const originalEndingDigits = Number(weiDecimalValue) % multiplier; + + // Generate random digits, but always greater than or equal to original ending digits + // This ensures we only randomize within the specified number of digits + const randomEndingDigits = + originalEndingDigits + + Math.floor(Math.random() * (multiplier - originalEndingDigits)); // Combine base and random parts - const randomisedWeiDecimal = basePart + randomDigits; + const randomisedWeiDecimal = basePart + randomEndingDigits; // Convert wei decimal to hex return `0x${randomisedWeiDecimal.toString(16)}` as Hex; From 369fa9ad11df5a18f5a3ef80ff01f47d0fc17ec5 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 15:30:59 +0100 Subject: [PATCH 12/24] Remove jsdoc examples --- .../RandomisedEstimationsGasFeeFlow.ts | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 793b55235d2..4fa9363985d 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -220,33 +220,6 @@ function getRandomisedGasFeeConfig( * @param gweiDecimalValue - The original gas fee value in Gwei (decimal) * @param [numberOfDigitsToRandomizeAtTheEnd] - The number of least significant digits to randomise * @returns The randomised value converted to Wei in hexadecimal format - * - * @example - * // Randomise last 3 digits of "5" Gwei (5000000000 Wei) - * randomiseDecimalGWEIAndConvertToHex("5", 3) - * // Decimal output range: 5000000000 to 5000000999 Wei - * // Hex output range: 0x12a05f200 to 0x12a05f3e7 - * - * @example - * // Randomise last 6 digits of "10.5" Gwei (10500000000 Wei) - * randomiseDecimalGWEIAndConvertToHex("10.5", 6) - * // Decimal output range: 10500000000 to 10500999999 Wei - * // Hex output range: 0x27312d600 to 0x27313f9cf - * - * @example - * // Randomise last 3 digits of value with existing decimals (5000500123 Wei) - * randomiseDecimalGWEIAndConvertToHex("5.000500123", 3) - * // Base part: 5000500000 Wei - * // Original ending digits: 123 - * // Random ending digits: 123-999 - * // Decimal output range: 5000500123 to 5000500999 Wei - * // Hex output range: 0x12a05f247b to 0x12a05f3e7 - * - * @example - * // Randomise last 9 digits of "42" Gwei (42000000000 Wei) - * randomiseDecimalGWEIAndConvertToHex("42", 9) - * // Decimal output range: 42000000000 to 42999999999 Wei - * // Hex output range: 0x9c7652400 to 0x9fffff9ff */ export function randomiseDecimalGWEIAndConvertToHex( gweiDecimalValue: string | number, From 6f1268044691267dbf684d048048b0b8e0d2a489 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 15:42:35 +0100 Subject: [PATCH 13/24] Fix lint --- .../RandomisedEstimationsGasFeeFlow.test.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 42e03237075..dd6e9349e73 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -387,7 +387,7 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { // The base part should be exactly 5.000 Gwei const basePart = (Math.floor(resultWei / 1000) * 1000) / 1e9; - expect(basePart).toEqual(5); + expect(basePart).toBe(5); }); it('ensures randomized value is never below original value', () => { @@ -456,19 +456,11 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { // For "0" input, the result should still be 0 // This is because 0 has no "ending digits" to randomize // The implementation will still start from 0 and only randomize upward - const result = randomiseDecimalGWEIAndConvertToHex('0', 3); const resultWei = parseInt(result.slice(2), 16); expect(resultWei).toBeGreaterThanOrEqual(0); - - if (resultWei === 0) { - // If it returns 0, that's valid - expect(resultWei).toBe(0); - } else { - // If it returns a randomized value, it should be in the expected range - expect(resultWei).toBeLessThanOrEqual(999); - } + expect(resultWei).toBeLessThanOrEqual(999); }); it('handles different number formats correctly', () => { From a97512d2b3db7c46d0b1ca693c2a31d4a98eca7e Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 20 Mar 2025 15:47:36 +0100 Subject: [PATCH 14/24] Fix lint --- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index dd6e9349e73..7854d56985a 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -466,6 +466,6 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { it('handles different number formats correctly', () => { const resultFromNumber = randomiseDecimalGWEIAndConvertToHex(5, 3); const resultFromString = randomiseDecimalGWEIAndConvertToHex('5', 3); - expect(resultFromNumber).toEqual(resultFromString); + expect(resultFromNumber).toStrictEqual(resultFromString); }); }); From ad08a1485b76ced283b41b255f65f0c1a3c22df8 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 21 Mar 2025 11:21:08 +0100 Subject: [PATCH 15/24] Update suggestions --- .../src/TransactionController.test.ts | 4 +- .../src/TransactionController.ts | 11 ++- .../src/gas-flows/DefaultGasFeeFlow.test.ts | 16 ++-- .../src/gas-flows/DefaultGasFeeFlow.ts | 2 +- .../src/gas-flows/LineaGasFeeFlow.test.ts | 8 +- .../src/gas-flows/LineaGasFeeFlow.ts | 8 +- .../OptimismLayer1GasFeeFlow.test.ts | 8 +- .../src/gas-flows/OptimismLayer1GasFeeFlow.ts | 8 +- .../gas-flows/OracleLayer1GasFeeFlow.test.ts | 2 +- .../src/gas-flows/OracleLayer1GasFeeFlow.ts | 9 +- .../RandomisedEstimationsGasFeeFlow.test.ts | 69 +++++--------- .../RandomisedEstimationsGasFeeFlow.ts | 91 ++++++++++--------- .../gas-flows/ScrollLayer1GasFeeFlow.test.ts | 8 +- .../src/gas-flows/ScrollLayer1GasFeeFlow.ts | 8 +- .../src/gas-flows/TestGasFeeFlow.test.ts | 8 +- .../src/gas-flows/TestGasFeeFlow.ts | 3 +- .../src/helpers/GasFeePoller.test.ts | 19 +--- .../src/helpers/GasFeePoller.ts | 7 +- packages/transaction-controller/src/types.ts | 29 ++++-- .../src/utils/feature-flags.test.ts | 42 +++++++++ .../src/utils/feature-flags.ts | 33 ++++--- .../src/utils/gas-fees.ts | 9 +- .../src/utils/gas-flow.test.ts | 12 +-- .../src/utils/gas-flow.ts | 6 +- .../src/utils/layer1-gas-fee-flow.test.ts | 7 ++ .../src/utils/layer1-gas-fee-flow.ts | 12 ++- 26 files changed, 262 insertions(+), 177 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index bc17eb95e08..4aec2ea4fb6 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -2014,7 +2014,7 @@ describe('TransactionController', () => { }); it('updates gas fee properties', async () => { - const { controller } = setupController({ + const { controller, messenger } = setupController({ options: { getCurrentNetworkEIP1559Compatibility: async () => true, getCurrentAccountEIP1559Compatibility: async () => true, @@ -2035,7 +2035,6 @@ describe('TransactionController', () => { expect(updateGasFeesMock).toHaveBeenCalledWith({ eip1559: true, ethQuery: expect.any(Object), - featureFlags: expect.any(Object), gasFeeFlows: [ randomisedEstimationsGasFeeFlowMock, lineaGasFeeFlowMock, @@ -2043,6 +2042,7 @@ describe('TransactionController', () => { ], getGasFeeEstimates: expect.any(Function), getSavedGasFees: expect.any(Function), + messenger: expect.any(Object), txMeta: expect.any(Object), }); }); diff --git a/packages/transaction-controller/src/TransactionController.ts b/packages/transaction-controller/src/TransactionController.ts index fe6e24ec81c..a513ee5c327 100644 --- a/packages/transaction-controller/src/TransactionController.ts +++ b/packages/transaction-controller/src/TransactionController.ts @@ -117,7 +117,6 @@ import { signAuthorizationList, } from './utils/eip7702'; import { validateConfirmedExternalTransaction } from './utils/external-transactions'; -import { getFeatureFlags } from './utils/feature-flags'; import { addGasBuffer, estimateGas, updateGas } from './utils/gas'; import { updateGasFees } from './utils/gas-fees'; import { getGasFeeFlow } from './utils/gas-flow'; @@ -1987,6 +1986,7 @@ export class TransactionController extends BaseController< await updateTransactionLayer1GasFee({ layer1GasFeeFlows: this.layer1GasFeeFlows, + messenger: this.messagingSystem, provider, transactionMeta: updatedTransaction, }); @@ -2286,7 +2286,6 @@ export class TransactionController extends BaseController< networkClientId: requestNetworkClientId, }); - const featureFlags = getFeatureFlags(this.messagingSystem); const transactionMeta = { txParams: transactionParams, chainId, @@ -2297,7 +2296,7 @@ export class TransactionController extends BaseController< const gasFeeFlow = getGasFeeFlow( transactionMeta, this.gasFeeFlows, - featureFlags, + this.messagingSystem, ) as GasFeeFlow; const ethQuery = new EthQuery(provider); @@ -2308,8 +2307,8 @@ export class TransactionController extends BaseController< return gasFeeFlow.getGasFees({ ethQuery, - featureFlags, gasFeeControllerData, + messenger: this.messagingSystem, transactionMeta, }); } @@ -2339,6 +2338,7 @@ export class TransactionController extends BaseController< return await getTransactionLayer1GasFee({ layer1GasFeeFlows: this.layer1GasFeeFlows, + messenger: this.messagingSystem, provider, transactionMeta: { txParams: transactionParams, @@ -2584,10 +2584,10 @@ export class TransactionController extends BaseController< await updateGasFees({ eip1559: isEIP1559Compatible, ethQuery, - featureFlags: getFeatureFlags(this.messagingSystem), gasFeeFlows: this.gasFeeFlows, getGasFeeEstimates: this.getGasFeeEstimates, getSavedGasFees: this.getSavedGasFees.bind(this), + messenger: this.messagingSystem, txMeta: transactionMeta, }), ); @@ -2597,6 +2597,7 @@ export class TransactionController extends BaseController< async () => await updateTransactionLayer1GasFee({ layer1GasFeeFlows: this.layer1GasFeeFlows, + messenger: this.messagingSystem, provider, transactionMeta, }), diff --git a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts index 849b2dd673c..c9c9907c1fa 100644 --- a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.test.ts @@ -8,6 +8,7 @@ import type { import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { FeeMarketGasFeeEstimates, GasPriceGasFeeEstimates, @@ -15,7 +16,6 @@ import type { TransactionMeta, } from '../types'; import { GasFeeEstimateType, TransactionStatus } from '../types'; -import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; const ETH_QUERY_MOCK = {} as EthQuery; @@ -51,8 +51,6 @@ const LEGACY_ESTIMATES_MOCK: LegacyGasPriceEstimate = { high: '5', }; -const FEATURE_FLAGS_MOCK = {} as TransactionControllerFeatureFlags; - const FEE_MARKET_RESPONSE_MOCK = { gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, gasFeeEstimates: FEE_MARKET_ESTIMATES_MOCK, @@ -102,9 +100,7 @@ describe('DefaultGasFeeFlow', () => { describe('matchesTransaction', () => { it('returns true', () => { const defaultGasFeeFlow = new DefaultGasFeeFlow(); - const result = defaultGasFeeFlow.matchesTransaction( - TRANSACTION_META_MOCK, - ); + const result = defaultGasFeeFlow.matchesTransaction(); expect(result).toBe(true); }); }); @@ -115,8 +111,8 @@ describe('DefaultGasFeeFlow', () => { const response = await defaultGasFeeFlow.getGasFees({ ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, gasFeeControllerData: FEE_MARKET_RESPONSE_MOCK, + messenger: {} as TransactionControllerMessenger, transactionMeta: TRANSACTION_META_MOCK, }); @@ -130,8 +126,8 @@ describe('DefaultGasFeeFlow', () => { const response = await defaultGasFeeFlow.getGasFees({ ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, gasFeeControllerData: LEGACY_RESPONSE_MOCK, + messenger: {} as TransactionControllerMessenger, transactionMeta: TRANSACTION_META_MOCK, }); @@ -145,8 +141,8 @@ describe('DefaultGasFeeFlow', () => { const response = await defaultGasFeeFlow.getGasFees({ ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, gasFeeControllerData: GAS_PRICE_RESPONSE_MOCK, + messenger: {} as TransactionControllerMessenger, transactionMeta: TRANSACTION_META_MOCK, }); @@ -160,10 +156,10 @@ describe('DefaultGasFeeFlow', () => { const response = defaultGasFeeFlow.getGasFees({ ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, gasFeeControllerData: { gasEstimateType: GAS_ESTIMATE_TYPES.NONE, } as GasFeeState, + messenger: {} as TransactionControllerMessenger, transactionMeta: TRANSACTION_META_MOCK, }); diff --git a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts index 6b351cb51c0..b2daa93c38d 100644 --- a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts @@ -28,7 +28,7 @@ const log = createModuleLogger(projectLogger, 'default-gas-fee-flow'); * The standard implementation of a gas fee flow that obtains gas fee estimates using only the GasFeeController. */ export class DefaultGasFeeFlow implements GasFeeFlow { - matchesTransaction(_transactionMeta: TransactionMeta): boolean { + matchesTransaction(): boolean { return true; } diff --git a/packages/transaction-controller/src/gas-flows/LineaGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/LineaGasFeeFlow.test.ts index 264595d0442..9f37a23d35e 100644 --- a/packages/transaction-controller/src/gas-flows/LineaGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/LineaGasFeeFlow.test.ts @@ -4,6 +4,7 @@ import type EthQuery from '@metamask/eth-query'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; import { LineaGasFeeFlow } from './LineaGasFeeFlow'; import { CHAIN_IDS } from '../constants'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { FeeMarketGasFeeEstimates, GasFeeFlowRequest, @@ -82,7 +83,12 @@ describe('LineaGasFeeFlow', () => { chainId, }; - expect(flow.matchesTransaction(transaction)).toBe(true); + expect( + flow.matchesTransaction({ + transactionMeta: transaction, + messenger: {} as TransactionControllerMessenger, + }), + ).toBe(true); }); }); diff --git a/packages/transaction-controller/src/gas-flows/LineaGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/LineaGasFeeFlow.ts index fb45f701d3b..bd9208d5699 100644 --- a/packages/transaction-controller/src/gas-flows/LineaGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/LineaGasFeeFlow.ts @@ -5,6 +5,7 @@ import type BN from 'bn.js'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; import { projectLogger } from '../logger'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { GasFeeEstimates, GasFeeFlow, @@ -49,7 +50,12 @@ const PRIORITY_FEE_MULTIPLIERS = { * - Static multipliers to increase the base and priority fees. */ export class LineaGasFeeFlow implements GasFeeFlow { - matchesTransaction(transactionMeta: TransactionMeta): boolean { + matchesTransaction({ + transactionMeta, + }: { + transactionMeta: TransactionMeta; + messenger: TransactionControllerMessenger; + }): boolean { return LINEA_CHAIN_IDS.includes(transactionMeta.chainId); } diff --git a/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.test.ts index 9fdf36dddee..d2722953e7e 100644 --- a/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.test.ts @@ -1,5 +1,6 @@ import { OptimismLayer1GasFeeFlow } from './OptimismLayer1GasFeeFlow'; import { CHAIN_IDS } from '../constants'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { TransactionMeta } from '../types'; import { TransactionStatus } from '../types'; @@ -28,7 +29,12 @@ describe('OptimismLayer1GasFeeFlow', () => { chainId, }; - expect(flow.matchesTransaction(transaction)).toBe(true); + expect( + flow.matchesTransaction({ + transactionMeta: transaction, + messenger: {} as TransactionControllerMessenger, + }), + ).toBe(true); }); }); }); diff --git a/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.ts index 4b4a9d1521e..27186d5e427 100644 --- a/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/OptimismLayer1GasFeeFlow.ts @@ -2,6 +2,7 @@ import { type Hex } from '@metamask/utils'; import { OracleLayer1GasFeeFlow } from './OracleLayer1GasFeeFlow'; import { CHAIN_IDS } from '../constants'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { TransactionMeta } from '../types'; const OPTIMISM_STACK_CHAIN_IDS: Hex[] = [ @@ -26,7 +27,12 @@ export class OptimismLayer1GasFeeFlow extends OracleLayer1GasFeeFlow { super(OPTIMISM_GAS_PRICE_ORACLE_ADDRESS); } - matchesTransaction(transactionMeta: TransactionMeta): boolean { + matchesTransaction({ + transactionMeta, + }: { + transactionMeta: TransactionMeta; + messenger: TransactionControllerMessenger; + }): boolean { return OPTIMISM_STACK_CHAIN_IDS.includes(transactionMeta.chainId); } } diff --git a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts index bbe153a651f..ba1ab2dc888 100644 --- a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.test.ts @@ -54,7 +54,7 @@ function createMockTypedTransaction(serializedBuffer: Buffer) { } class MockOracleLayer1GasFeeFlow extends OracleLayer1GasFeeFlow { - matchesTransaction(_transactionMeta: TransactionMeta): boolean { + matchesTransaction(): boolean { return true; } } diff --git a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts index 8a3394ea2c2..918575dbad3 100644 --- a/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/OracleLayer1GasFeeFlow.ts @@ -4,6 +4,7 @@ import type { Hex } from '@metamask/utils'; import { createModuleLogger } from '@metamask/utils'; import { projectLogger } from '../logger'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { Layer1GasFeeFlow, Layer1GasFeeFlowRequest, @@ -40,7 +41,13 @@ export abstract class OracleLayer1GasFeeFlow implements Layer1GasFeeFlow { this.#signTransaction = signTransaction ?? false; } - abstract matchesTransaction(transactionMeta: TransactionMeta): boolean; + abstract matchesTransaction({ + transactionMeta, + messenger, + }: { + transactionMeta: TransactionMeta; + messenger: TransactionControllerMessenger; + }): boolean; async getLayer1Fee( request: Layer1GasFeeFlowRequest, diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 7854d56985a..468edb01eb0 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -8,6 +8,7 @@ import { RandomisedEstimationsGasFeeFlow, randomiseDecimalGWEIAndConvertToHex, } from './RandomisedEstimationsGasFeeFlow'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { FeeMarketGasFeeEstimates, GasPriceGasFeeEstimates, @@ -19,10 +20,10 @@ import { GasFeeEstimateType, TransactionStatus, } from '../types'; -import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; -import { FEATURE_FLAG_RANDOMISE_GAS_FEES } from '../utils/feature-flags'; +import { getRandomisedGasFeeDigits } from '../utils/feature-flags'; jest.mock('./DefaultGasFeeFlow'); +jest.mock('../utils/feature-flags'); // Mock Math.random to return predictable values const originalRandom = global.Math.random; @@ -39,15 +40,6 @@ const TRANSACTION_META_MOCK: TransactionMeta = { }, }; -const FEATURE_FLAGS_MOCK: TransactionControllerFeatureFlags = { - [FEATURE_FLAG_RANDOMISE_GAS_FEES]: { - config: { - '0x1': 6, - '0x5': 4, - }, - }, -}; - const ETH_QUERY_MOCK = {} as EthQuery; const DEFAULT_FEE_MARKET_RESPONSE: FeeMarketGasFeeEstimates = { @@ -79,6 +71,8 @@ const DEFAULT_GAS_PRICE_RESPONSE: GasPriceGasFeeEstimates = { }; describe('RandomisedEstimationsGasFeeFlow', () => { + const getRandomisedGasFeeDigitsMock = jest.mocked(getRandomisedGasFeeDigits); + beforeEach(() => { jest.resetAllMocks(); jest @@ -96,6 +90,8 @@ describe('RandomisedEstimationsGasFeeFlow', () => { } return { estimates: DEFAULT_GAS_PRICE_RESPONSE }; }); + + getRandomisedGasFeeDigitsMock.mockReturnValue(6); }); afterEach(() => { @@ -111,25 +107,16 @@ describe('RandomisedEstimationsGasFeeFlow', () => { chainId: '0x1', } as TransactionMeta; - expect(flow.matchesTransaction(transaction, FEATURE_FLAGS_MOCK)).toBe( - true, - ); + expect( + flow.matchesTransaction({ + transactionMeta: transaction, + messenger: {} as TransactionControllerMessenger, + }), + ).toBe(true); }); it('returns false if chainId is not in the randomisation config', () => { - const flow = new RandomisedEstimationsGasFeeFlow(); - - const transaction = { - ...TRANSACTION_META_MOCK, - chainId: '0x89', // Not in config - } as TransactionMeta; - - expect(flow.matchesTransaction(transaction, FEATURE_FLAGS_MOCK)).toBe( - false, - ); - }); - - it('returns false if feature flag is not exists', () => { + getRandomisedGasFeeDigitsMock.mockReturnValue(undefined); const flow = new RandomisedEstimationsGasFeeFlow(); const transaction = { @@ -138,10 +125,10 @@ describe('RandomisedEstimationsGasFeeFlow', () => { } as TransactionMeta; expect( - flow.matchesTransaction( - transaction, - undefined as unknown as TransactionControllerFeatureFlags, - ), + flow.matchesTransaction({ + transactionMeta: transaction, + messenger: {} as TransactionControllerMessenger, + }), ).toBe(false); }); }); @@ -152,7 +139,6 @@ describe('RandomisedEstimationsGasFeeFlow', () => { const request = { ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, transactionMeta: TRANSACTION_META_MOCK, gasFeeControllerData: { gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, @@ -172,6 +158,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { }, estimatedGasFeeTimeBounds: {}, } as GasFeeState, + messenger: {} as TransactionControllerMessenger, }; const result = await flow.getGasFees(request); @@ -228,12 +215,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { const request = { ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, - // Using 0x5 with 4 digits randomization - transactionMeta: { - ...TRANSACTION_META_MOCK, - chainId: '0x5', - } as TransactionMeta, + transactionMeta: TRANSACTION_META_MOCK, gasFeeControllerData: { gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, gasFeeEstimates: { @@ -242,6 +224,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { high: '300000', }, } as GasFeeState, + messenger: {} as TransactionControllerMessenger, }; const result = await flow.getGasFees(request); @@ -262,8 +245,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { // Verify value is within expected range expect(actualValue).not.toBe(originalValue); expect(actualValue).toBeGreaterThanOrEqual(originalValue); - // For 4 digits randomization (defined in FEATURE_FLAGS_MOCK for '0x5') - expect(actualValue).toBeLessThanOrEqual(originalValue + 9999); + expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); } }); @@ -272,7 +254,6 @@ describe('RandomisedEstimationsGasFeeFlow', () => { const request = { ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, transactionMeta: TRANSACTION_META_MOCK, gasFeeControllerData: { gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, @@ -280,6 +261,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { gasPrice: '200000', }, } as GasFeeState, + messenger: {} as TransactionControllerMessenger, }; const result = await flow.getGasFees(request); @@ -294,7 +276,6 @@ describe('RandomisedEstimationsGasFeeFlow', () => { // Verify gas price is within expected range expect(actualValue).not.toBe(originalValue); expect(actualValue).toBeGreaterThanOrEqual(originalValue); - // For 6 digits randomization (defined in FEATURE_FLAGS_MOCK for '0x1') expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); }); @@ -308,7 +289,6 @@ describe('RandomisedEstimationsGasFeeFlow', () => { const request = { ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, transactionMeta: TRANSACTION_META_MOCK, gasFeeControllerData: { gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, @@ -328,6 +308,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { }, estimatedGasFeeTimeBounds: {}, } as GasFeeState, + messenger: {} as TransactionControllerMessenger, }; const result = await flow.getGasFees(request); @@ -344,12 +325,12 @@ describe('RandomisedEstimationsGasFeeFlow', () => { const request = { ethQuery: ETH_QUERY_MOCK, - featureFlags: FEATURE_FLAGS_MOCK, transactionMeta: TRANSACTION_META_MOCK, gasFeeControllerData: { gasEstimateType: 'UNSUPPORTED_TYPE', gasFeeEstimates: {}, } as unknown as GasFeeState, + messenger: {} as TransactionControllerMessenger, }; // Capture the error in a spy so we can verify default flow was called diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 4fa9363985d..56096dc646f 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -8,6 +8,7 @@ import { createModuleLogger, type Hex } from '@metamask/utils'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; import { projectLogger } from '../logger'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { FeeMarketGasFeeEstimateForLevel, FeeMarketGasFeeEstimates, @@ -20,8 +21,7 @@ import type { TransactionMeta, } from '../types'; import { GasFeeEstimateLevel, GasFeeEstimateType } from '../types'; -import type { TransactionControllerFeatureFlags } from '../utils/feature-flags'; -import { FEATURE_FLAG_RANDOMISE_GAS_FEES } from '../utils/feature-flags'; +import { getRandomisedGasFeeDigits } from '../utils/feature-flags'; import { gweiDecimalToWeiDecimal } from '../utils/gas-fees'; const log = createModuleLogger( @@ -30,23 +30,26 @@ const log = createModuleLogger( ); const PRESERVE_NUMBER_OF_DIGITS = 2; -const DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE = 4; /** * Implementation of a gas fee flow that randomises the last digits of gas fee estimations */ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { - matchesTransaction( - transactionMeta: TransactionMeta, - featureFlags: TransactionControllerFeatureFlags, - ): boolean { + matchesTransaction({ + transactionMeta, + messenger, + }: { + transactionMeta: TransactionMeta; + messenger: TransactionControllerMessenger; + }): boolean { const { chainId } = transactionMeta; - const randomiseGasFeesConfig = getRandomisedGasFeeConfig(featureFlags); - - const enabledChainIds = Object.keys(randomiseGasFeesConfig); + const randomisedGasFeeDigits = getRandomisedGasFeeDigits( + chainId, + messenger, + ); - return enabledChainIds.includes(chainId); + return randomisedGasFeeDigits !== undefined; } async getGasFees(request: GasFeeFlowRequest): Promise { @@ -67,11 +70,13 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { async #getRandomisedGasFees( request: GasFeeFlowRequest, ): Promise { - const { featureFlags, gasFeeControllerData, transactionMeta } = request; + const { messenger, gasFeeControllerData, transactionMeta } = request; const { gasEstimateType, gasFeeEstimates } = gasFeeControllerData; - const randomiseGasFeesConfig = getRandomisedGasFeeConfig(featureFlags); - const lastNDigits = randomiseGasFeesConfig[transactionMeta.chainId]; + const randomisedGasFeeDigits = getRandomisedGasFeeDigits( + transactionMeta.chainId, + messenger, + ) as number; let response: GasFeeEstimates; @@ -80,7 +85,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { log('Using fee market estimates', gasFeeEstimates); response = this.#randomiseFeeMarketEstimates( gasFeeEstimates, - lastNDigits, + randomisedGasFeeDigits, ); log('Randomised fee market estimates', response); break; @@ -88,7 +93,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { log('Using legacy estimates', gasFeeEstimates); response = this.#randomiseLegacyEstimates( gasFeeEstimates as LegacyGasPriceEstimate, - lastNDigits, + randomisedGasFeeDigits, ); log('Randomised legacy estimates', response); break; @@ -96,7 +101,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { log('Using eth_gasPrice estimates', gasFeeEstimates); response = this.#getRandomisedGasPriceEstimate( gasFeeEstimates as EthGasPriceEstimate, - lastNDigits, + randomisedGasFeeDigits, ); log('Randomised eth_gasPrice estimates', response); break; @@ -196,18 +201,15 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { } /** - * Returns the randomised gas fee config from the feature flags + * Generates a random number with the specified number of digits that is greater than or equal to the given minimum value. * - * @param featureFlags - All feature flags - * @returns The randomised gas fee config + * @param digitCount - The number of digits the random number should have + * @param minValue - The minimum value the random number should have + * @returns A random number with the specified number of digits */ -function getRandomisedGasFeeConfig( - featureFlags: TransactionControllerFeatureFlags, -): Record { - const randomiseGasFeesConfig = - featureFlags?.[FEATURE_FLAG_RANDOMISE_GAS_FEES]?.config ?? {}; - - return randomiseGasFeesConfig; +function generateRandomDigits(digitCount: number, minValue: number): number { + const multiplier = 10 ** digitCount; + return minValue + Math.floor(Math.random() * (multiplier - minValue)); } /** @@ -223,7 +225,7 @@ function getRandomisedGasFeeConfig( */ export function randomiseDecimalGWEIAndConvertToHex( gweiDecimalValue: string | number, - numberOfDigitsToRandomizeAtTheEnd = DEFAULT_NUMBER_OF_DIGITS_TO_RANDOMISE, + numberOfDigitsToRandomizeAtTheEnd: number, ): Hex { const weiDecimalValue = gweiDecimalToWeiDecimal(gweiDecimalValue); const decimalLength = weiDecimalValue.length; @@ -234,24 +236,29 @@ export function randomiseDecimalGWEIAndConvertToHex( decimalLength - PRESERVE_NUMBER_OF_DIGITS, ); - const multiplier = 10 ** effectiveDigitsToRandomise; + // Handle the case when the value is 0 or too small + if (Number(weiDecimalValue) === 0 || effectiveDigitsToRandomise <= 0) { + return `0x${Number(weiDecimalValue).toString(16)}` as Hex; + } - // Keep the original value up to the digits we want to preserve - const basePart = - Math.floor(Number(weiDecimalValue) / multiplier) * multiplier; + // Use string manipulation to get the base part (significant digits) + const significantDigitsCount = decimalLength - effectiveDigitsToRandomise; + const significantDigits = weiDecimalValue.slice(0, significantDigitsCount); - // Get the original ending digits - const originalEndingDigits = Number(weiDecimalValue) % multiplier; + // Get the original ending digits using string manipulation + const endingDigits = weiDecimalValue.slice(-effectiveDigitsToRandomise); + const originalEndingDigits = Number(endingDigits); - // Generate random digits, but always greater than or equal to original ending digits - // This ensures we only randomize within the specified number of digits - const randomEndingDigits = - originalEndingDigits + - Math.floor(Math.random() * (multiplier - originalEndingDigits)); + // Generate random digits that are greater than or equal to the original ending digits + const randomEndingDigits = generateRandomDigits( + effectiveDigitsToRandomise, + originalEndingDigits, + ); - // Combine base and random parts - const randomisedWeiDecimal = basePart + randomEndingDigits; + const basePart = BigInt( + significantDigits + '0'.repeat(effectiveDigitsToRandomise), + ); + const randomisedWeiDecimal = basePart + BigInt(randomEndingDigits); - // Convert wei decimal to hex return `0x${randomisedWeiDecimal.toString(16)}` as Hex; } diff --git a/packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.test.ts index 4451a1ab1c5..2c19516f207 100644 --- a/packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.test.ts @@ -1,5 +1,6 @@ import { ScrollLayer1GasFeeFlow } from './ScrollLayer1GasFeeFlow'; import { CHAIN_IDS } from '../constants'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { TransactionMeta } from '../types'; import { TransactionStatus } from '../types'; @@ -28,7 +29,12 @@ describe('ScrollLayer1GasFeeFlow', () => { chainId, }; - expect(flow.matchesTransaction(transaction)).toBe(true); + expect( + flow.matchesTransaction({ + transactionMeta: transaction, + messenger: {} as TransactionControllerMessenger, + }), + ).toBe(true); }); }); }); diff --git a/packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.ts index 63c0cc66c24..0298bcebaad 100644 --- a/packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/ScrollLayer1GasFeeFlow.ts @@ -2,6 +2,7 @@ import { type Hex } from '@metamask/utils'; import { OracleLayer1GasFeeFlow } from './OracleLayer1GasFeeFlow'; import { CHAIN_IDS } from '../constants'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { TransactionMeta } from '../types'; const SCROLL_CHAIN_IDS: Hex[] = [CHAIN_IDS.SCROLL, CHAIN_IDS.SCROLL_SEPOLIA]; @@ -18,7 +19,12 @@ export class ScrollLayer1GasFeeFlow extends OracleLayer1GasFeeFlow { super(SCROLL_GAS_PRICE_ORACLE_ADDRESS, true); } - matchesTransaction(transactionMeta: TransactionMeta): boolean { + matchesTransaction({ + transactionMeta, + }: { + transactionMeta: TransactionMeta; + messenger: TransactionControllerMessenger; + }): boolean { return SCROLL_CHAIN_IDS.includes(transactionMeta.chainId); } } diff --git a/packages/transaction-controller/src/gas-flows/TestGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/TestGasFeeFlow.test.ts index 6c24952ecac..44a5f77c75d 100644 --- a/packages/transaction-controller/src/gas-flows/TestGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/TestGasFeeFlow.test.ts @@ -1,15 +1,11 @@ import { TestGasFeeFlow } from './TestGasFeeFlow'; -import { - GasFeeEstimateType, - type GasFeeFlowRequest, - type TransactionMeta, -} from '../types'; +import { GasFeeEstimateType, type GasFeeFlowRequest } from '../types'; describe('TestGasFeeFlow', () => { describe('matchesTransaction', () => { it('should return true', () => { const testGasFeeFlow = new TestGasFeeFlow(); - const result = testGasFeeFlow.matchesTransaction({} as TransactionMeta); + const result = testGasFeeFlow.matchesTransaction(); expect(result).toBe(true); }); }); diff --git a/packages/transaction-controller/src/gas-flows/TestGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/TestGasFeeFlow.ts index 718c4a6bbfc..1c5d63dce8a 100644 --- a/packages/transaction-controller/src/gas-flows/TestGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/TestGasFeeFlow.ts @@ -6,7 +6,6 @@ import { type GasFeeFlow, type GasFeeFlowRequest, type GasFeeFlowResponse, - type TransactionMeta, } from '../types'; const INCREMENT = 1e15; // 0.001 ETH @@ -20,7 +19,7 @@ const LEVEL_DIFFERENCE = 0.5; export class TestGasFeeFlow implements GasFeeFlow { #counter = 1; - matchesTransaction(_transactionMeta: TransactionMeta): boolean { + matchesTransaction(): boolean { return true; } diff --git a/packages/transaction-controller/src/helpers/GasFeePoller.test.ts b/packages/transaction-controller/src/helpers/GasFeePoller.test.ts index e1dbb2faa12..cbe007ff348 100644 --- a/packages/transaction-controller/src/helpers/GasFeePoller.test.ts +++ b/packages/transaction-controller/src/helpers/GasFeePoller.test.ts @@ -15,10 +15,6 @@ import { type GasFeeEstimates, type TransactionMeta, } from '../types'; -import { - getFeatureFlags, - type TransactionControllerFeatureFlags, -} from '../utils/feature-flags'; import { getTransactionLayer1GasFee } from '../utils/layer1-gas-fee-flow'; jest.mock('../utils/feature-flags'); @@ -84,16 +80,11 @@ describe('GasFeePoller', () => { const getGasFeeControllerEstimatesMock = jest.fn(); const findNetworkClientIdByChainIdMock = jest.fn(); const messengerMock = jest.fn() as unknown as TransactionControllerMessenger; - const getFeatureFlagsMock = jest.mocked(getFeatureFlags); beforeEach(() => { jest.clearAllTimers(); jest.clearAllMocks(); - getFeatureFlagsMock.mockReturnValue( - {} as unknown as TransactionControllerFeatureFlags, - ); - gasFeeFlowMock = createGasFeeFlowMock(); gasFeeFlowMock.matchesTransaction.mockReturnValue(true); gasFeeFlowMock.getGasFees.mockResolvedValue(GAS_FEE_FLOW_RESPONSE_MOCK); @@ -138,13 +129,6 @@ describe('GasFeePoller', () => { }); it('calls gas fee flow', async () => { - const mockFeatureFlags = { - test: { - config: {}, - }, - } as unknown as TransactionControllerFeatureFlags; - - getFeatureFlagsMock.mockReturnValue(mockFeatureFlags); getGasFeeControllerEstimatesMock.mockResolvedValue({}); new GasFeePoller(constructorOptions); @@ -155,8 +139,8 @@ describe('GasFeePoller', () => { expect(gasFeeFlowMock.getGasFees).toHaveBeenCalledTimes(1); expect(gasFeeFlowMock.getGasFees).toHaveBeenCalledWith({ ethQuery: expect.any(Object), - featureFlags: mockFeatureFlags, gasFeeControllerData: {}, + messenger: expect.any(Function), transactionMeta: TRANSACTION_META_MOCK, }); }); @@ -171,6 +155,7 @@ describe('GasFeePoller', () => { expect(getTransactionLayer1GasFeeMock).toHaveBeenCalledWith({ provider: expect.any(Object), layer1GasFeeFlows: layer1GasFeeFlowsMock, + messenger: expect.any(Function), transactionMeta: TRANSACTION_META_MOCK, }); }); diff --git a/packages/transaction-controller/src/helpers/GasFeePoller.ts b/packages/transaction-controller/src/helpers/GasFeePoller.ts index 4186a00e6c4..b77de08c962 100644 --- a/packages/transaction-controller/src/helpers/GasFeePoller.ts +++ b/packages/transaction-controller/src/helpers/GasFeePoller.ts @@ -28,7 +28,6 @@ import { TransactionStatus, TransactionEnvelopeType, } from '../types'; -import { getFeatureFlags } from '../utils/feature-flags'; import { getGasFeeFlow } from '../utils/gas-flow'; import { getTransactionLayer1GasFee } from '../utils/layer1-gas-fee-flow'; @@ -214,12 +213,11 @@ export class GasFeePoller { > { const { networkClientId } = transactionMeta; - const featureFlags = getFeatureFlags(this.#messenger); const ethQuery = new EthQuery(this.#getProvider(networkClientId)); const gasFeeFlow = getGasFeeFlow( transactionMeta, this.#gasFeeFlows, - featureFlags, + this.#messenger, ); if (gasFeeFlow) { @@ -232,8 +230,8 @@ export class GasFeePoller { const request: GasFeeFlowRequest = { ethQuery, - featureFlags, gasFeeControllerData, + messenger: this.#messenger, transactionMeta, }; @@ -268,6 +266,7 @@ export class GasFeePoller { const layer1GasFee = await getTransactionLayer1GasFee({ layer1GasFeeFlows: this.#layer1GasFeeFlows, + messenger: this.#messenger, provider, transactionMeta, }); diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 612921544a5..d450358c411 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -6,7 +6,7 @@ import type { NetworkClientId, Provider } from '@metamask/network-controller'; import type { Hex, Json } from '@metamask/utils'; import type { Operation } from 'fast-json-patch'; -import type { TransactionControllerFeatureFlags } from './utils/feature-flags'; +import type { TransactionControllerMessenger } from './TransactionController'; /** * Given a record, ensures that each property matches the `Json` type. @@ -1212,12 +1212,12 @@ export type GasFeeFlowRequest = { /** An EthQuery instance to enable queries to the associated RPC provider. */ ethQuery: EthQuery; - /** The feature flags for the transaction controller. */ - featureFlags: TransactionControllerFeatureFlags; - /** Gas fee controller data matching the chain ID of the transaction. */ gasFeeControllerData: GasFeeState; + /** The messenger instance. */ + messenger: TransactionControllerMessenger; + /** The metadata of the transaction to obtain estimates for. */ transactionMeta: TransactionMeta; }; @@ -1234,12 +1234,16 @@ export type GasFeeFlow = { * Determine if the gas fee flow supports the specified transaction. * * @param transactionMeta - The transaction metadata. + * @param messenger - The messenger instance. * @returns Whether the gas fee flow supports the transaction. */ - matchesTransaction( - transactionMeta: TransactionMeta, - featureFlags: TransactionControllerFeatureFlags, - ): boolean; + matchesTransaction({ + transactionMeta, + messenger, + }: { + transactionMeta: TransactionMeta; + messenger: TransactionControllerMessenger; + }): boolean; /** * Get gas fee estimates for a specific transaction. @@ -1271,9 +1275,16 @@ export type Layer1GasFeeFlow = { * Determine if the gas fee flow supports the specified transaction. * * @param transactionMeta - The transaction metadata. + * @param messenger - The messenger instance. * @returns Whether the layer1 gas fee flow supports the transaction. */ - matchesTransaction(transactionMeta: TransactionMeta): boolean; + matchesTransaction({ + transactionMeta, + messenger, + }: { + transactionMeta: TransactionMeta; + messenger: TransactionControllerMessenger; + }): boolean; /** * Get layer 1 gas fee estimates for a specific transaction. diff --git a/packages/transaction-controller/src/utils/feature-flags.test.ts b/packages/transaction-controller/src/utils/feature-flags.test.ts index 0b888d6b940..9073224cf58 100644 --- a/packages/transaction-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-controller/src/utils/feature-flags.test.ts @@ -11,6 +11,7 @@ import { getEIP7702ContractAddresses, getEIP7702SupportedChains, getEIP7702UpgradeContractAddress, + getRandomisedGasFeeDigits, } from './feature-flags'; import { isValidSignature } from './signature'; import type { TransactionControllerMessenger } from '..'; @@ -428,4 +429,45 @@ describe('Feature Flags Utils', () => { }); }); }); + + describe('getRandomisedGasFeeDigits', () => { + it('returns value from remote feature flag controller', () => { + mockFeatureFlags({ + [FEATURE_FLAG_TRANSACTIONS]: { + randomisedGasFeeDigits: { + [CHAIN_ID_MOCK]: 3, + [CHAIN_ID_2_MOCK]: 2, + }, + }, + }); + + expect( + getRandomisedGasFeeDigits(CHAIN_ID_MOCK, controllerMessenger), + ).toBe(3); + expect( + getRandomisedGasFeeDigits(CHAIN_ID_2_MOCK, controllerMessenger), + ).toBe(2); + }); + + it('returns undefined if no randomised gas fee digits for chain', () => { + mockFeatureFlags({ + [FEATURE_FLAG_TRANSACTIONS]: { + randomisedGasFeeDigits: { + [CHAIN_ID_2_MOCK]: 2, + }, + }, + }); + + expect( + getRandomisedGasFeeDigits(CHAIN_ID_MOCK, controllerMessenger), + ).toBeUndefined(); + }); + + it('returns undefined if feature flag not configured', () => { + mockFeatureFlags({}); + expect( + getRandomisedGasFeeDigits(CHAIN_ID_MOCK, controllerMessenger), + ).toBeUndefined(); + }); + }); }); diff --git a/packages/transaction-controller/src/utils/feature-flags.ts b/packages/transaction-controller/src/utils/feature-flags.ts index 1f7ccceeb50..a9d19aae9b6 100644 --- a/packages/transaction-controller/src/utils/feature-flags.ts +++ b/packages/transaction-controller/src/utils/feature-flags.ts @@ -6,8 +6,6 @@ import type { TransactionControllerMessenger } from '../TransactionController'; export const FEATURE_FLAG_TRANSACTIONS = 'confirmations_transactions'; export const FEATURE_FLAG_EIP_7702 = 'confirmations_eip_7702'; -export const FEATURE_FLAG_RANDOMISE_GAS_FEES = - 'confirmations-randomise-gas-fees'; const DEFAULT_BATCH_SIZE_LIMIT = 10; const DEFAULT_ACCELERATED_POLLING_COUNT_MAX = 10; @@ -65,15 +63,9 @@ export type TransactionControllerFeatureFlags = { /** Default `intervalMs` in case no chain-specific parameter is set. */ defaultIntervalMs?: number; }; - }; - [FEATURE_FLAG_RANDOMISE_GAS_FEES]?: { - /** - * Config for randomizing gas fees. - * Keyed by chain ID. - * Value is the number of digits to randomise. - */ - config?: Record; + /** Randomised gas fee digits. */ + randomisedGasFeeDigits?: Record; }; }; @@ -185,13 +177,32 @@ export function getAcceleratedPollingParams( return { countMax, intervalMs }; } +/** + * Retrieves the number of digits to randomise for a given chain ID. + * + * @param chainId - The chain ID. + * @param messenger - The controller messenger instance. + * @returns The number of digits to randomise. + */ +export function getRandomisedGasFeeDigits( + chainId: Hex, + messenger: TransactionControllerMessenger, +): number | undefined { + const featureFlags = getFeatureFlags(messenger); + + const randomisedGasFeeDigits = + featureFlags?.[FEATURE_FLAG_TRANSACTIONS]?.randomisedGasFeeDigits ?? {}; + + return randomisedGasFeeDigits[chainId]; +} + /** * Retrieves the relevant feature flags from the remote feature flag controller. * * @param messenger - The messenger instance. * @returns The feature flags. */ -export function getFeatureFlags( +function getFeatureFlags( messenger: TransactionControllerMessenger, ): TransactionControllerFeatureFlags { const featureFlags = messenger.call( diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index 4eb731d9fbc..2997a2a9819 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -16,6 +16,7 @@ import type { TransactionControllerFeatureFlags } from './feature-flags'; import { getGasFeeFlow } from './gas-flow'; import { SWAP_TRANSACTION_TYPES } from './swaps'; import { projectLogger } from '../logger'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { SavedGasFees, TransactionParams, @@ -28,12 +29,12 @@ import { GasFeeEstimateType, UserFeeLevel } from '../types'; export type UpdateGasFeesRequest = { eip1559: boolean; ethQuery: EthQuery; - featureFlags: TransactionControllerFeatureFlags; gasFeeFlows: GasFeeFlow[]; getGasFeeEstimates: ( options: FetchGasFeeEstimateOptions, ) => Promise; getSavedGasFees: (chainId: Hex) => SavedGasFees | undefined; + messenger: TransactionControllerMessenger; txMeta: TransactionMeta; }; @@ -351,9 +352,9 @@ async function getSuggestedGasFees( const { eip1559, ethQuery, - featureFlags, gasFeeFlows, getGasFeeEstimates, + messenger, txMeta, } = request; @@ -371,7 +372,7 @@ async function getSuggestedGasFees( const gasFeeFlow = getGasFeeFlow( txMeta, gasFeeFlows, - featureFlags, + messenger, ) as GasFeeFlow; try { @@ -379,8 +380,8 @@ async function getSuggestedGasFees( const response = await gasFeeFlow.getGasFees({ ethQuery, - featureFlags, gasFeeControllerData, + messenger, transactionMeta: txMeta, }); diff --git a/packages/transaction-controller/src/utils/gas-flow.test.ts b/packages/transaction-controller/src/utils/gas-flow.test.ts index 4b4b5ae2a99..a6faffe8362 100644 --- a/packages/transaction-controller/src/utils/gas-flow.test.ts +++ b/packages/transaction-controller/src/utils/gas-flow.test.ts @@ -1,7 +1,7 @@ import type { GasFeeEstimates as GasFeeControllerEstimates } from '@metamask/gas-fee-controller'; -import type { TransactionControllerFeatureFlags } from './feature-flags'; import { getGasFeeFlow, mergeGasFeeEstimates } from './gas-flow'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { FeeMarketGasFeeEstimates, GasFeeFlow, @@ -87,12 +87,6 @@ function createGasFeeFlowMock(): jest.Mocked { describe('gas-flow', () => { describe('getGasFeeFlow', () => { - const featureFlags = { - test: { - config: {}, - }, - } as unknown as TransactionControllerFeatureFlags; - it('returns undefined if no gas fee flow matches transaction', () => { const gasFeeFlow1 = createGasFeeFlowMock(); const gasFeeFlow2 = createGasFeeFlowMock(); @@ -104,7 +98,7 @@ describe('gas-flow', () => { getGasFeeFlow( TRANSACTION_META_MOCK, [gasFeeFlow1, gasFeeFlow2], - featureFlags, + {} as TransactionControllerMessenger, ), ).toBeUndefined(); }); @@ -120,7 +114,7 @@ describe('gas-flow', () => { getGasFeeFlow( TRANSACTION_META_MOCK, [gasFeeFlow1, gasFeeFlow2], - featureFlags, + {} as TransactionControllerMessenger, ), ).toBe(gasFeeFlow2); }); diff --git a/packages/transaction-controller/src/utils/gas-flow.ts b/packages/transaction-controller/src/utils/gas-flow.ts index 822b169ffde..49e158fed83 100644 --- a/packages/transaction-controller/src/utils/gas-flow.ts +++ b/packages/transaction-controller/src/utils/gas-flow.ts @@ -8,6 +8,7 @@ import type { import { type GasFeeState } from '@metamask/gas-fee-controller'; import type { TransactionControllerFeatureFlags } from './feature-flags'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { FeeMarketGasFeeEstimates, GasPriceGasFeeEstimates, @@ -38,15 +39,16 @@ type MergeGasFeeEstimatesRequest = { * @param transactionMeta - The transaction metadata to find a gas fee flow for. * @param gasFeeFlows - The gas fee flows to search. * @param featureFlags - All feature flags + * @param messenger * @returns The first gas fee flow that matches the transaction, or undefined if none match. */ export function getGasFeeFlow( transactionMeta: TransactionMeta, gasFeeFlows: GasFeeFlow[], - featureFlags: TransactionControllerFeatureFlags, + messenger: TransactionControllerMessenger, ): GasFeeFlow | undefined { return gasFeeFlows.find((gasFeeFlow) => - gasFeeFlow.matchesTransaction(transactionMeta, featureFlags), + gasFeeFlow.matchesTransaction({ transactionMeta, messenger }), ); } diff --git a/packages/transaction-controller/src/utils/layer1-gas-fee-flow.test.ts b/packages/transaction-controller/src/utils/layer1-gas-fee-flow.test.ts index a5027bc4d35..ad39439c5f8 100644 --- a/packages/transaction-controller/src/utils/layer1-gas-fee-flow.test.ts +++ b/packages/transaction-controller/src/utils/layer1-gas-fee-flow.test.ts @@ -2,6 +2,7 @@ import type { Provider } from '@metamask/network-controller'; import type { Hex } from '@metamask/utils'; import { updateTransactionLayer1GasFee } from './layer1-gas-fee-flow'; +import type { TransactionControllerMessenger } from '../TransactionController'; import { TransactionStatus, type Layer1GasFeeFlow, @@ -41,6 +42,7 @@ describe('updateTransactionLayer1GasFee', () => { let layer1GasFeeFlowsMock: jest.Mocked; let providerMock: Provider; let transactionMetaMock: TransactionMeta; + let messengerMock: TransactionControllerMessenger; beforeEach(() => { layer1GasFeeFlowsMock = [ @@ -66,11 +68,14 @@ describe('updateTransactionLayer1GasFee', () => { from: '0x123', }, }; + + messengerMock = {} as TransactionControllerMessenger; }); it('updates given transaction layer1GasFee property', async () => { await updateTransactionLayer1GasFee({ layer1GasFeeFlows: layer1GasFeeFlowsMock, + messenger: messengerMock, provider: providerMock, transactionMeta: transactionMetaMock, }); @@ -101,6 +106,7 @@ describe('updateTransactionLayer1GasFee', () => { await updateTransactionLayer1GasFee({ layer1GasFeeFlows: layer1GasFeeFlowsMock, + messenger: messengerMock, provider: providerMock, transactionMeta: transactionMetaMock, }); @@ -121,6 +127,7 @@ describe('updateTransactionLayer1GasFee', () => { await updateTransactionLayer1GasFee({ layer1GasFeeFlows: layer1GasFeeFlowsMock, + messenger: messengerMock, provider: providerMock, transactionMeta: transactionMetaMock, }); diff --git a/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts b/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts index a311a60b20e..22f4d3f25dc 100644 --- a/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts +++ b/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts @@ -2,12 +2,14 @@ import type { Provider } from '@metamask/network-controller'; import { createModuleLogger, type Hex } from '@metamask/utils'; import { projectLogger } from '../logger'; +import type { TransactionControllerMessenger } from '../TransactionController'; import type { Layer1GasFeeFlow, TransactionMeta } from '../types'; const log = createModuleLogger(projectLogger, 'layer-1-gas-fee-flow'); export type UpdateLayer1GasFeeRequest = { layer1GasFeeFlows: Layer1GasFeeFlow[]; + messenger: TransactionControllerMessenger; provider: Provider; transactionMeta: TransactionMeta; }; @@ -41,14 +43,19 @@ export async function updateTransactionLayer1GasFee( * * @param transactionMeta - The transaction to get the layer 1 gas fee flow for. * @param layer1GasFeeFlows - The layer 1 gas fee flows to search. + * @param messenger * @returns The layer 1 gas fee flow for the transaction, or undefined if none match. */ function getLayer1GasFeeFlow( transactionMeta: TransactionMeta, layer1GasFeeFlows: Layer1GasFeeFlow[], + messenger: TransactionControllerMessenger, ): Layer1GasFeeFlow | undefined { return layer1GasFeeFlows.find((layer1GasFeeFlow) => - layer1GasFeeFlow.matchesTransaction(transactionMeta), + layer1GasFeeFlow.matchesTransaction({ + transactionMeta, + messenger, + }), ); } @@ -59,16 +66,19 @@ function getLayer1GasFeeFlow( * @param request.layer1GasFeeFlows - The layer 1 gas fee flows to search. * @param request.provider - The provider to use to get the layer 1 gas fee. * @param request.transactionMeta - The transaction to get the layer 1 gas fee for. + * @param request.messenger * @returns The layer 1 gas fee. */ export async function getTransactionLayer1GasFee({ layer1GasFeeFlows, + messenger, provider, transactionMeta, }: UpdateLayer1GasFeeRequest): Promise { const layer1GasFeeFlow = getLayer1GasFeeFlow( transactionMeta, layer1GasFeeFlows, + messenger, ); if (!layer1GasFeeFlow) { From 918ec5237fff3f3e31893cae7a19bd0011f68129 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 21 Mar 2025 11:29:56 +0100 Subject: [PATCH 16/24] Add final suggestions --- .../RandomisedEstimationsGasFeeFlow.test.ts | 110 +++++++++--------- .../RandomisedEstimationsGasFeeFlow.ts | 50 ++++---- 2 files changed, 78 insertions(+), 82 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 468edb01eb0..527c9aa7661 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -134,39 +134,39 @@ describe('RandomisedEstimationsGasFeeFlow', () => { }); describe('getGasFees', () => { - it('randomises fee market estimates for chain IDs in the feature flag config', async () => { - const flow = new RandomisedEstimationsGasFeeFlow(); - - const request = { - ethQuery: ETH_QUERY_MOCK, - transactionMeta: TRANSACTION_META_MOCK, - gasFeeControllerData: { - gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, - gasFeeEstimates: { - low: { - suggestedMaxFeePerGas: '100000', - suggestedMaxPriorityFeePerGas: '100000', + it.each(Object.values(GasFeeEstimateLevel))( + 'randomises fee market estimates for %s level', + async (level) => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const request = { + ethQuery: ETH_QUERY_MOCK, + transactionMeta: TRANSACTION_META_MOCK, + gasFeeControllerData: { + gasEstimateType: GAS_ESTIMATE_TYPES.FEE_MARKET, + gasFeeEstimates: { + low: { + suggestedMaxFeePerGas: '100000', + suggestedMaxPriorityFeePerGas: '100000', + }, + medium: { + suggestedMaxFeePerGas: '200000', + suggestedMaxPriorityFeePerGas: '200000', + }, + high: { + suggestedMaxFeePerGas: '300000', + suggestedMaxPriorityFeePerGas: '300000', + }, }, - medium: { - suggestedMaxFeePerGas: '200000', - suggestedMaxPriorityFeePerGas: '200000', - }, - high: { - suggestedMaxFeePerGas: '300000', - suggestedMaxPriorityFeePerGas: '300000', - }, - }, - estimatedGasFeeTimeBounds: {}, - } as GasFeeState, - messenger: {} as TransactionControllerMessenger, - }; + estimatedGasFeeTimeBounds: {}, + } as GasFeeState, + messenger: {} as TransactionControllerMessenger, + }; - const result = await flow.getGasFees(request); + const result = await flow.getGasFees(request); - expect(result.estimates.type).toBe(GasFeeEstimateType.FeeMarket); + expect(result.estimates.type).toBe(GasFeeEstimateType.FeeMarket); - // For all levels, verify that randomization occurred but stayed within expected range - for (const level of Object.values(GasFeeEstimateLevel)) { const estimates = request.gasFeeControllerData .gasFeeEstimates as Record< GasFeeEstimateLevel, @@ -207,33 +207,33 @@ describe('RandomisedEstimationsGasFeeFlow', () => { expect(actualPriorityValue).toBeLessThanOrEqual( originalPriorityValue + 999999, ); - } - }); - - it('randomises legacy estimates with specified digits', async () => { - const flow = new RandomisedEstimationsGasFeeFlow(); - - const request = { - ethQuery: ETH_QUERY_MOCK, - transactionMeta: TRANSACTION_META_MOCK, - gasFeeControllerData: { - gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, - gasFeeEstimates: { - low: '100000', - medium: '200000', - high: '300000', - }, - } as GasFeeState, - messenger: {} as TransactionControllerMessenger, - }; + }, + ); + + it.each(Object.values(GasFeeEstimateLevel))( + 'randomises legacy estimates for %s level', + async (level) => { + const flow = new RandomisedEstimationsGasFeeFlow(); + + const request = { + ethQuery: ETH_QUERY_MOCK, + transactionMeta: TRANSACTION_META_MOCK, + gasFeeControllerData: { + gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, + gasFeeEstimates: { + low: '100000', + medium: '200000', + high: '300000', + }, + } as GasFeeState, + messenger: {} as TransactionControllerMessenger, + }; - const result = await flow.getGasFees(request); + const result = await flow.getGasFees(request); - // Verify result type - expect(result.estimates.type).toBe(GasFeeEstimateType.Legacy); + // Verify result type + expect(result.estimates.type).toBe(GasFeeEstimateType.Legacy); - // For all levels, verify that randomization occurred but stayed within expected range - for (const level of Object.values(GasFeeEstimateLevel)) { const gasHex = (result.estimates as LegacyGasFeeEstimates)[level]; const estimates = request.gasFeeControllerData .gasFeeEstimates as Record; @@ -246,8 +246,8 @@ describe('RandomisedEstimationsGasFeeFlow', () => { expect(actualValue).not.toBe(originalValue); expect(actualValue).toBeGreaterThanOrEqual(originalValue); expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); - } - }); + }, + ); it('randomises eth_gasPrice estimates', async () => { const flow = new RandomisedEstimationsGasFeeFlow(); diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 56096dc646f..b389d4ed06a 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -80,33 +80,29 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { let response: GasFeeEstimates; - switch (gasEstimateType) { - case GAS_ESTIMATE_TYPES.FEE_MARKET: - log('Using fee market estimates', gasFeeEstimates); - response = this.#randomiseFeeMarketEstimates( - gasFeeEstimates, - randomisedGasFeeDigits, - ); - log('Randomised fee market estimates', response); - break; - case GAS_ESTIMATE_TYPES.LEGACY: - log('Using legacy estimates', gasFeeEstimates); - response = this.#randomiseLegacyEstimates( - gasFeeEstimates as LegacyGasPriceEstimate, - randomisedGasFeeDigits, - ); - log('Randomised legacy estimates', response); - break; - case GAS_ESTIMATE_TYPES.ETH_GASPRICE: - log('Using eth_gasPrice estimates', gasFeeEstimates); - response = this.#getRandomisedGasPriceEstimate( - gasFeeEstimates as EthGasPriceEstimate, - randomisedGasFeeDigits, - ); - log('Randomised eth_gasPrice estimates', response); - break; - default: - throw new Error(`Unsupported gas estimate type: ${gasEstimateType}`); + if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { + log('Using fee market estimates', gasFeeEstimates); + response = this.#randomiseFeeMarketEstimates( + gasFeeEstimates, + randomisedGasFeeDigits, + ); + log('Randomised fee market estimates', response); + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) { + log('Using legacy estimates', gasFeeEstimates); + response = this.#randomiseLegacyEstimates( + gasFeeEstimates, + randomisedGasFeeDigits, + ); + log('Randomised legacy estimates', response); + } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) { + log('Using eth_gasPrice estimates', gasFeeEstimates); + response = this.#getRandomisedGasPriceEstimate( + gasFeeEstimates, + randomisedGasFeeDigits, + ); + log('Randomised eth_gasPrice estimates', response); + } else { + throw new Error(`Unsupported gas estimate type: ${gasEstimateType}`); } return { From 83738cb866298ccb30bc5502e3ed96e3f42097a1 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 21 Mar 2025 11:31:02 +0100 Subject: [PATCH 17/24] Remove unnecessary comment --- .../src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 527c9aa7661..704576959a5 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -190,7 +190,6 @@ describe('RandomisedEstimationsGasFeeFlow', () => { // For 6 digits randomization in FEATURE_FLAGS_MOCK for '0x1' expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); - // Same approach for maxPriorityFeePerGas const maxPriorityFeeHex = ( result.estimates as FeeMarketGasFeeEstimates )[level].maxPriorityFeePerGas; From 253693d7cc16691468c3b69fadb235a5daf93cea Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 21 Mar 2025 12:07:49 +0100 Subject: [PATCH 18/24] Fix lint --- .../transaction-controller/src/TransactionController.test.ts | 2 +- .../src/gas-flows/DefaultGasFeeFlow.ts | 1 - packages/transaction-controller/src/types.ts | 5 +++-- packages/transaction-controller/src/utils/gas-fees.ts | 1 - packages/transaction-controller/src/utils/gas-flow.ts | 4 +--- .../transaction-controller/src/utils/layer1-gas-fee-flow.ts | 2 +- 6 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/transaction-controller/src/TransactionController.test.ts b/packages/transaction-controller/src/TransactionController.test.ts index 4aec2ea4fb6..a6e386ef774 100644 --- a/packages/transaction-controller/src/TransactionController.test.ts +++ b/packages/transaction-controller/src/TransactionController.test.ts @@ -2014,7 +2014,7 @@ describe('TransactionController', () => { }); it('updates gas fee properties', async () => { - const { controller, messenger } = setupController({ + const { controller } = setupController({ options: { getCurrentNetworkEIP1559Compatibility: async () => true, getCurrentAccountEIP1559Compatibility: async () => true, diff --git a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts index b2daa93c38d..84ae34102c4 100644 --- a/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/DefaultGasFeeFlow.ts @@ -17,7 +17,6 @@ import type { GasFeeFlowResponse, GasPriceGasFeeEstimates, LegacyGasFeeEstimates, - TransactionMeta, } from '../types'; import { GasFeeEstimateLevel, GasFeeEstimateType } from '../types'; import { gweiDecimalToWeiHex } from '../utils/gas-fees'; diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index d450358c411..d28cb344b02 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -1233,8 +1233,9 @@ export type GasFeeFlow = { /** * Determine if the gas fee flow supports the specified transaction. * - * @param transactionMeta - The transaction metadata. - * @param messenger - The messenger instance. + * @param args - The arguments for the matcher function. + * @param args.transactionMeta - The transaction metadata. + * @param args.messenger - The messenger instance. * @returns Whether the gas fee flow supports the transaction. */ matchesTransaction({ diff --git a/packages/transaction-controller/src/utils/gas-fees.ts b/packages/transaction-controller/src/utils/gas-fees.ts index 2997a2a9819..50a7f64f5f9 100644 --- a/packages/transaction-controller/src/utils/gas-fees.ts +++ b/packages/transaction-controller/src/utils/gas-fees.ts @@ -12,7 +12,6 @@ import type { import type { Hex } from '@metamask/utils'; import { add0x, createModuleLogger } from '@metamask/utils'; -import type { TransactionControllerFeatureFlags } from './feature-flags'; import { getGasFeeFlow } from './gas-flow'; import { SWAP_TRANSACTION_TYPES } from './swaps'; import { projectLogger } from '../logger'; diff --git a/packages/transaction-controller/src/utils/gas-flow.ts b/packages/transaction-controller/src/utils/gas-flow.ts index 49e158fed83..a641c74dc12 100644 --- a/packages/transaction-controller/src/utils/gas-flow.ts +++ b/packages/transaction-controller/src/utils/gas-flow.ts @@ -7,7 +7,6 @@ import type { } from '@metamask/gas-fee-controller'; import { type GasFeeState } from '@metamask/gas-fee-controller'; -import type { TransactionControllerFeatureFlags } from './feature-flags'; import type { TransactionControllerMessenger } from '../TransactionController'; import type { FeeMarketGasFeeEstimates, @@ -38,8 +37,7 @@ type MergeGasFeeEstimatesRequest = { * * @param transactionMeta - The transaction metadata to find a gas fee flow for. * @param gasFeeFlows - The gas fee flows to search. - * @param featureFlags - All feature flags - * @param messenger + * @param messenger - The messenger instance. * @returns The first gas fee flow that matches the transaction, or undefined if none match. */ export function getGasFeeFlow( diff --git a/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts b/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts index 22f4d3f25dc..6e8b92af91d 100644 --- a/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts +++ b/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts @@ -66,7 +66,7 @@ function getLayer1GasFeeFlow( * @param request.layer1GasFeeFlows - The layer 1 gas fee flows to search. * @param request.provider - The provider to use to get the layer 1 gas fee. * @param request.transactionMeta - The transaction to get the layer 1 gas fee for. - * @param request.messenger + * @param request.messenger - The messenger instance. * @returns The layer 1 gas fee. */ export async function getTransactionLayer1GasFee({ From 5e421c0a2e9011f53d4e6e346c33a75680635314 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 21 Mar 2025 12:14:48 +0100 Subject: [PATCH 19/24] Fix lint --- packages/transaction-controller/src/types.ts | 9 +++++---- .../src/utils/layer1-gas-fee-flow.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index d28cb344b02..3dd964f7f2a 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -1274,10 +1274,11 @@ export type Layer1GasFeeFlowResponse = { export type Layer1GasFeeFlow = { /** * Determine if the gas fee flow supports the specified transaction. - * - * @param transactionMeta - The transaction metadata. - * @param messenger - The messenger instance. - * @returns Whether the layer1 gas fee flow supports the transaction. + * + * @param args - The arguments for the matcher function. + * @param args.transactionMeta - The transaction metadata. + * @param args.messenger - The messenger instance. + * @returns Whether the gas fee flow supports the transaction. */ matchesTransaction({ transactionMeta, diff --git a/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts b/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts index 6e8b92af91d..ff11cb4a958 100644 --- a/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts +++ b/packages/transaction-controller/src/utils/layer1-gas-fee-flow.ts @@ -43,7 +43,7 @@ export async function updateTransactionLayer1GasFee( * * @param transactionMeta - The transaction to get the layer 1 gas fee flow for. * @param layer1GasFeeFlows - The layer 1 gas fee flows to search. - * @param messenger + * @param messenger - The messenger instance. * @returns The layer 1 gas fee flow for the transaction, or undefined if none match. */ function getLayer1GasFeeFlow( From d2c995f0e5726f8160d5c55ffd321fe2c0c0c23a Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Fri, 21 Mar 2025 12:36:28 +0100 Subject: [PATCH 20/24] Fix lint --- packages/transaction-controller/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-controller/src/types.ts b/packages/transaction-controller/src/types.ts index 3dd964f7f2a..b132c55a5e7 100644 --- a/packages/transaction-controller/src/types.ts +++ b/packages/transaction-controller/src/types.ts @@ -1274,7 +1274,7 @@ export type Layer1GasFeeFlowResponse = { export type Layer1GasFeeFlow = { /** * Determine if the gas fee flow supports the specified transaction. - * + * * @param args - The arguments for the matcher function. * @param args.transactionMeta - The transaction metadata. * @param args.messenger - The messenger instance. From 4a42eecf49dfddb6556bf92a21375cade33093db Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 27 Mar 2025 13:28:55 +0100 Subject: [PATCH 21/24] Update --- .../RandomisedEstimationsGasFeeFlow.test.ts | 40 ++++----- .../RandomisedEstimationsGasFeeFlow.ts | 90 +++++++++---------- .../src/utils/feature-flags.test.ts | 22 +++++ .../src/utils/feature-flags.ts | 20 +++++ 4 files changed, 105 insertions(+), 67 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 704576959a5..3fb2672d84e 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -20,7 +20,10 @@ import { GasFeeEstimateType, TransactionStatus, } from '../types'; -import { getRandomisedGasFeeDigits } from '../utils/feature-flags'; +import { + getPreserveNumberOfDigitsForRandomisedGasFee, + getRandomisedGasFeeDigits, +} from '../utils/feature-flags'; jest.mock('./DefaultGasFeeFlow'); jest.mock('../utils/feature-flags'); @@ -72,6 +75,9 @@ const DEFAULT_GAS_PRICE_RESPONSE: GasPriceGasFeeEstimates = { describe('RandomisedEstimationsGasFeeFlow', () => { const getRandomisedGasFeeDigitsMock = jest.mocked(getRandomisedGasFeeDigits); + const getPreserveNumberOfDigitsForRandomisedGasFeeMock = jest.mocked( + getPreserveNumberOfDigitsForRandomisedGasFee, + ); beforeEach(() => { jest.resetAllMocks(); @@ -92,6 +98,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { }); getRandomisedGasFeeDigitsMock.mockReturnValue(6); + getPreserveNumberOfDigitsForRandomisedGasFeeMock.mockReturnValue(2); }); afterEach(() => { @@ -135,7 +142,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { describe('getGasFees', () => { it.each(Object.values(GasFeeEstimateLevel))( - 'randomises fee market estimates for %s level', + 'randomises only priority fee for fee market estimates for %s level', async (level) => { const flow = new RandomisedEstimationsGasFeeFlow(); @@ -179,16 +186,10 @@ describe('RandomisedEstimationsGasFeeFlow', () => { const maxFeeHex = (result.estimates as FeeMarketGasFeeEstimates)[level] .maxFeePerGas; - // Get the actual value for comparison only + // Verify that the maxFeePerGas is not randomised const originalValue = Number(estimates[level].suggestedMaxFeePerGas); const actualValue = parseInt(maxFeeHex.slice(2), 16) / 1e9; - - // Just verify the value changed and is within range - expect(actualValue).not.toBe(originalValue); - expect(actualValue).toBeGreaterThanOrEqual(originalValue); - - // For 6 digits randomization in FEATURE_FLAGS_MOCK for '0x1' - expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); + expect(actualValue).toBe(originalValue); const maxPriorityFeeHex = ( result.estimates as FeeMarketGasFeeEstimates @@ -210,7 +211,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { ); it.each(Object.values(GasFeeEstimateLevel))( - 'randomises legacy estimates for %s level', + 'does not randomise legacy estimates for %s level', async (level) => { const flow = new RandomisedEstimationsGasFeeFlow(); @@ -237,18 +238,15 @@ describe('RandomisedEstimationsGasFeeFlow', () => { const estimates = request.gasFeeControllerData .gasFeeEstimates as Record; - // Convert hex to decimal for easier comparison + // Verify that the gas price is not randomised const originalValue = Number(estimates[level]); const actualValue = parseInt(gasHex.slice(2), 16) / 1e9; - // Verify value is within expected range - expect(actualValue).not.toBe(originalValue); - expect(actualValue).toBeGreaterThanOrEqual(originalValue); - expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); + expect(actualValue).toBe(originalValue); }, ); - it('randomises eth_gasPrice estimates', async () => { + it('does not randomise eth_gasPrice estimates', async () => { const flow = new RandomisedEstimationsGasFeeFlow(); const request = { @@ -273,12 +271,10 @@ describe('RandomisedEstimationsGasFeeFlow', () => { const actualValue = parseInt(gasHex.slice(2), 16) / 1e9; // Verify gas price is within expected range - expect(actualValue).not.toBe(originalValue); - expect(actualValue).toBeGreaterThanOrEqual(originalValue); - expect(actualValue).toBeLessThanOrEqual(originalValue + 999999); + expect(actualValue).toBe(originalValue); }); - it('should fall back to default flow if randomization fails', async () => { + it('fall backs to default flow if randomization fails', async () => { const flow = new RandomisedEstimationsGasFeeFlow(); // Mock Math.random to throw an error @@ -319,7 +315,7 @@ describe('RandomisedEstimationsGasFeeFlow', () => { expect(result.estimates).toStrictEqual(DEFAULT_FEE_MARKET_RESPONSE); }); - it('should throw an error for unsupported gas estimate types', async () => { + it('throws an error for unsupported gas estimate types', async () => { const flow = new RandomisedEstimationsGasFeeFlow(); const request = { diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index b389d4ed06a..f2f31c433d8 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -4,7 +4,7 @@ import type { EthGasPriceEstimate, } from '@metamask/gas-fee-controller'; import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; -import { createModuleLogger, type Hex } from '@metamask/utils'; +import { add0x, createModuleLogger, type Hex } from '@metamask/utils'; import { DefaultGasFeeFlow } from './DefaultGasFeeFlow'; import { projectLogger } from '../logger'; @@ -21,15 +21,21 @@ import type { TransactionMeta, } from '../types'; import { GasFeeEstimateLevel, GasFeeEstimateType } from '../types'; -import { getRandomisedGasFeeDigits } from '../utils/feature-flags'; -import { gweiDecimalToWeiDecimal } from '../utils/gas-fees'; +import { + getPreserveNumberOfDigitsForRandomisedGasFee, + getRandomisedGasFeeDigits, +} from '../utils/feature-flags'; +import { + gweiDecimalToWeiDecimal, + gweiDecimalToWeiHex, +} from '../utils/gas-fees'; const log = createModuleLogger( projectLogger, 'randomised-estimation-gas-fee-flow', ); -const PRESERVE_NUMBER_OF_DIGITS = 2; +const DEFAULT_PRESERVE_NUMBER_OF_DIGITS = 2; /** * Implementation of a gas fee flow that randomises the last digits of gas fee estimations @@ -54,7 +60,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { async getGasFees(request: GasFeeFlowRequest): Promise { try { - return await this.#getRandomisedGasFees(request); + return this.#getRandomisedGasFees(request); } catch (error) { log('Using default flow as fallback due to error', error); return await this.#getDefaultGasFees(request); @@ -67,9 +73,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { return new DefaultGasFeeFlow().getGasFees(request); } - async #getRandomisedGasFees( - request: GasFeeFlowRequest, - ): Promise { + #getRandomisedGasFees(request: GasFeeFlowRequest): GasFeeFlowResponse { const { messenger, gasFeeControllerData, transactionMeta } = request; const { gasEstimateType, gasFeeEstimates } = gasFeeControllerData; @@ -78,29 +82,27 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { messenger, ) as number; + const preservedNumberOfDigits = + getPreserveNumberOfDigitsForRandomisedGasFee(messenger); + let response: GasFeeEstimates; if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { log('Using fee market estimates', gasFeeEstimates); - response = this.#randomiseFeeMarketEstimates( + response = this.#getRandomisedFeeMarketEstimates( gasFeeEstimates, randomisedGasFeeDigits, + preservedNumberOfDigits, ); - log('Randomised fee market estimates', response); + log('Added randomised fee market estimates', response); } else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) { log('Using legacy estimates', gasFeeEstimates); - response = this.#randomiseLegacyEstimates( - gasFeeEstimates, - randomisedGasFeeDigits, - ); - log('Randomised legacy estimates', response); + response = this.#getLegacyEstimates(gasFeeEstimates); + log('Added legacy estimates', response); } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) { log('Using eth_gasPrice estimates', gasFeeEstimates); - response = this.#getRandomisedGasPriceEstimate( - gasFeeEstimates, - randomisedGasFeeDigits, - ); - log('Randomised eth_gasPrice estimates', response); + response = this.#getGasPriceEstimates(gasFeeEstimates); + log('Added eth_gasPrice estimates', response); } else { throw new Error(`Unsupported gas estimate type: ${gasEstimateType}`); } @@ -110,9 +112,10 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { }; } - #randomiseFeeMarketEstimates( + #getRandomisedFeeMarketEstimates( gasFeeEstimates: FeeMarketGasPriceEstimate, lastNDigits: number, + preservedNumberOfDigits?: number, ): FeeMarketGasFeeEstimates { const levels = Object.values(GasFeeEstimateLevel).reduce( (result, level) => ({ @@ -121,6 +124,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { gasFeeEstimates, level, lastNDigits, + preservedNumberOfDigits, ), }), {} as Omit, @@ -136,31 +140,28 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { gasFeeEstimates: FeeMarketGasPriceEstimate, level: GasFeeEstimateLevel, lastNDigits: number, + preservedNumberOfDigits?: number, ): FeeMarketGasFeeEstimateForLevel { return { - maxFeePerGas: randomiseDecimalGWEIAndConvertToHex( + maxFeePerGas: gweiDecimalToWeiHex( gasFeeEstimates[level].suggestedMaxFeePerGas, - lastNDigits, ), + // Only priority fee is randomised maxPriorityFeePerGas: randomiseDecimalGWEIAndConvertToHex( gasFeeEstimates[level].suggestedMaxPriorityFeePerGas, lastNDigits, + preservedNumberOfDigits, ), }; } - #randomiseLegacyEstimates( + #getLegacyEstimates( gasFeeEstimates: LegacyGasPriceEstimate, - lastNDigits: number, ): LegacyGasFeeEstimates { const levels = Object.values(GasFeeEstimateLevel).reduce( (result, level) => ({ ...result, - [level]: this.#getRandomisedLegacyLevel( - gasFeeEstimates, - level, - lastNDigits, - ), + [level]: this.#getLegacyLevel(gasFeeEstimates, level), }), {} as Omit, ); @@ -171,27 +172,19 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { }; } - #getRandomisedLegacyLevel( + #getLegacyLevel( gasFeeEstimates: LegacyGasPriceEstimate, level: GasFeeEstimateLevel, - lastNDigits: number, ): Hex { - return randomiseDecimalGWEIAndConvertToHex( - gasFeeEstimates[level], - lastNDigits, - ); + return gweiDecimalToWeiHex(gasFeeEstimates[level]); } - #getRandomisedGasPriceEstimate( + #getGasPriceEstimates( gasFeeEstimates: EthGasPriceEstimate, - lastNDigits: number, ): GasPriceGasFeeEstimates { return { type: GasFeeEstimateType.GasPrice, - gasPrice: randomiseDecimalGWEIAndConvertToHex( - gasFeeEstimates.gasPrice, - lastNDigits, - ), + gasPrice: gweiDecimalToWeiHex(gasFeeEstimates.gasPrice), }; } } @@ -216,20 +209,25 @@ function generateRandomDigits(digitCount: number, minValue: number): number { * The randomisation is performed in Wei units for more precision. * * @param gweiDecimalValue - The original gas fee value in Gwei (decimal) - * @param [numberOfDigitsToRandomizeAtTheEnd] - The number of least significant digits to randomise + * @param numberOfDigitsToRandomizeAtTheEnd - The number of least significant digits to randomise + * @param preservedNumberOfDigits - The number of most significant digits to preserve * @returns The randomised value converted to Wei in hexadecimal format */ export function randomiseDecimalGWEIAndConvertToHex( gweiDecimalValue: string | number, numberOfDigitsToRandomizeAtTheEnd: number, + preservedNumberOfDigits?: number, ): Hex { const weiDecimalValue = gweiDecimalToWeiDecimal(gweiDecimalValue); const decimalLength = weiDecimalValue.length; + const preservedDigits = + preservedNumberOfDigits ?? DEFAULT_PRESERVE_NUMBER_OF_DIGITS; + // Determine how many digits to randomise while preserving the PRESERVE_NUMBER_OF_DIGITS const effectiveDigitsToRandomise = Math.min( numberOfDigitsToRandomizeAtTheEnd, - decimalLength - PRESERVE_NUMBER_OF_DIGITS, + decimalLength - preservedDigits, ); // Handle the case when the value is 0 or too small @@ -256,5 +254,7 @@ export function randomiseDecimalGWEIAndConvertToHex( ); const randomisedWeiDecimal = basePart + BigInt(randomEndingDigits); - return `0x${randomisedWeiDecimal.toString(16)}` as Hex; + const hexRandomisedWei = `0x${randomisedWeiDecimal.toString(16)}`; + + return add0x(hexRandomisedWei); } diff --git a/packages/transaction-controller/src/utils/feature-flags.test.ts b/packages/transaction-controller/src/utils/feature-flags.test.ts index 9073224cf58..4a1e3120b93 100644 --- a/packages/transaction-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-controller/src/utils/feature-flags.test.ts @@ -11,6 +11,7 @@ import { getEIP7702ContractAddresses, getEIP7702SupportedChains, getEIP7702UpgradeContractAddress, + getPreserveNumberOfDigitsForRandomisedGasFee, getRandomisedGasFeeDigits, } from './feature-flags'; import { isValidSignature } from './signature'; @@ -470,4 +471,25 @@ describe('Feature Flags Utils', () => { ).toBeUndefined(); }); }); + + describe('getPreserveNumberOfDigitsForRandomisedGasFee', () => { + it('returns value from remote feature flag controller', () => { + mockFeatureFlags({ + [FEATURE_FLAG_TRANSACTIONS]: { + preservedNumberOfDigits: 5, + }, + }); + + expect( + getPreserveNumberOfDigitsForRandomisedGasFee(controllerMessenger), + ).toBe(5); + }); + + it('returns undefined if feature flag not configured', () => { + mockFeatureFlags({}); + expect( + getPreserveNumberOfDigitsForRandomisedGasFee(controllerMessenger), + ).toBeUndefined(); + }); + }); }); diff --git a/packages/transaction-controller/src/utils/feature-flags.ts b/packages/transaction-controller/src/utils/feature-flags.ts index a9d19aae9b6..ac5c883991f 100644 --- a/packages/transaction-controller/src/utils/feature-flags.ts +++ b/packages/transaction-controller/src/utils/feature-flags.ts @@ -66,6 +66,9 @@ export type TransactionControllerFeatureFlags = { /** Randomised gas fee digits. */ randomisedGasFeeDigits?: Record; + + /** Number of digits to preserve for randomised gas fee digits. */ + preservedNumberOfDigits?: number; }; }; @@ -196,6 +199,23 @@ export function getRandomisedGasFeeDigits( return randomisedGasFeeDigits[chainId]; } +/** + * Retrieves the number of digits to preserve for randomised gas fee digits. + * + * @param messenger - The controller messenger instance. + * @returns The number of digits to preserve. + */ +export function getPreserveNumberOfDigitsForRandomisedGasFee( + messenger: TransactionControllerMessenger, +): number | undefined { + const featureFlags = getFeatureFlags(messenger); + + const preservedNumberOfDigits = + featureFlags?.[FEATURE_FLAG_TRANSACTIONS]?.preservedNumberOfDigits; + + return preservedNumberOfDigits; +} + /** * Retrieves the relevant feature flags from the remote feature flag controller. * From ed6fdded9d65d586cf8664d2d0673fd375c03de4 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 27 Mar 2025 14:08:20 +0100 Subject: [PATCH 22/24] combine feature flags --- .../RandomisedEstimationsGasFeeFlow.test.ts | 23 +++--- .../RandomisedEstimationsGasFeeFlow.ts | 24 +++--- .../src/utils/feature-flags.test.ts | 75 ++++++++----------- .../src/utils/feature-flags.ts | 49 +++++------- 4 files changed, 71 insertions(+), 100 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 3fb2672d84e..5d580d85776 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -20,10 +20,7 @@ import { GasFeeEstimateType, TransactionStatus, } from '../types'; -import { - getPreserveNumberOfDigitsForRandomisedGasFee, - getRandomisedGasFeeDigits, -} from '../utils/feature-flags'; +import { getGasFeeRandomisation } from '../utils/feature-flags'; jest.mock('./DefaultGasFeeFlow'); jest.mock('../utils/feature-flags'); @@ -74,10 +71,7 @@ const DEFAULT_GAS_PRICE_RESPONSE: GasPriceGasFeeEstimates = { }; describe('RandomisedEstimationsGasFeeFlow', () => { - const getRandomisedGasFeeDigitsMock = jest.mocked(getRandomisedGasFeeDigits); - const getPreserveNumberOfDigitsForRandomisedGasFeeMock = jest.mocked( - getPreserveNumberOfDigitsForRandomisedGasFee, - ); + const getGasFeeRandomisationMock = jest.mocked(getGasFeeRandomisation); beforeEach(() => { jest.resetAllMocks(); @@ -97,8 +91,12 @@ describe('RandomisedEstimationsGasFeeFlow', () => { return { estimates: DEFAULT_GAS_PRICE_RESPONSE }; }); - getRandomisedGasFeeDigitsMock.mockReturnValue(6); - getPreserveNumberOfDigitsForRandomisedGasFeeMock.mockReturnValue(2); + getGasFeeRandomisationMock.mockReturnValue({ + randomisedGasFeeDigits: { + '0x1': 6, + }, + preservedNumberOfDigits: 2, + }); }); afterEach(() => { @@ -123,7 +121,10 @@ describe('RandomisedEstimationsGasFeeFlow', () => { }); it('returns false if chainId is not in the randomisation config', () => { - getRandomisedGasFeeDigitsMock.mockReturnValue(undefined); + getGasFeeRandomisationMock.mockReturnValue({ + randomisedGasFeeDigits: {}, + preservedNumberOfDigits: undefined, + }); const flow = new RandomisedEstimationsGasFeeFlow(); const transaction = { diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index f2f31c433d8..f0b8000c545 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -21,10 +21,7 @@ import type { TransactionMeta, } from '../types'; import { GasFeeEstimateLevel, GasFeeEstimateType } from '../types'; -import { - getPreserveNumberOfDigitsForRandomisedGasFee, - getRandomisedGasFeeDigits, -} from '../utils/feature-flags'; +import { getGasFeeRandomisation } from '../utils/feature-flags'; import { gweiDecimalToWeiDecimal, gweiDecimalToWeiHex, @@ -50,10 +47,10 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { }): boolean { const { chainId } = transactionMeta; - const randomisedGasFeeDigits = getRandomisedGasFeeDigits( - chainId, - messenger, - ); + const gasFeeRandomisation = getGasFeeRandomisation(messenger); + + const randomisedGasFeeDigits = + gasFeeRandomisation.randomisedGasFeeDigits[chainId]; return randomisedGasFeeDigits !== undefined; } @@ -77,13 +74,12 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { const { messenger, gasFeeControllerData, transactionMeta } = request; const { gasEstimateType, gasFeeEstimates } = gasFeeControllerData; - const randomisedGasFeeDigits = getRandomisedGasFeeDigits( - transactionMeta.chainId, - messenger, - ) as number; + const gasFeeRandomisation = getGasFeeRandomisation(messenger); + + const randomisedGasFeeDigits = + gasFeeRandomisation.randomisedGasFeeDigits[transactionMeta.chainId]; - const preservedNumberOfDigits = - getPreserveNumberOfDigitsForRandomisedGasFee(messenger); + const preservedNumberOfDigits = gasFeeRandomisation.preservedNumberOfDigits; let response: GasFeeEstimates; diff --git a/packages/transaction-controller/src/utils/feature-flags.test.ts b/packages/transaction-controller/src/utils/feature-flags.test.ts index 4a1e3120b93..5423d7a78d6 100644 --- a/packages/transaction-controller/src/utils/feature-flags.test.ts +++ b/packages/transaction-controller/src/utils/feature-flags.test.ts @@ -11,8 +11,7 @@ import { getEIP7702ContractAddresses, getEIP7702SupportedChains, getEIP7702UpgradeContractAddress, - getPreserveNumberOfDigitsForRandomisedGasFee, - getRandomisedGasFeeDigits, + getGasFeeRandomisation, } from './feature-flags'; import { isValidSignature } from './signature'; import type { TransactionControllerMessenger } from '..'; @@ -431,65 +430,51 @@ describe('Feature Flags Utils', () => { }); }); - describe('getRandomisedGasFeeDigits', () => { - it('returns value from remote feature flag controller', () => { - mockFeatureFlags({ - [FEATURE_FLAG_TRANSACTIONS]: { - randomisedGasFeeDigits: { - [CHAIN_ID_MOCK]: 3, - [CHAIN_ID_2_MOCK]: 2, - }, - }, - }); + describe('getGasFeeRandomisation', () => { + it('returns empty objects if no feature flags set', () => { + mockFeatureFlags({}); - expect( - getRandomisedGasFeeDigits(CHAIN_ID_MOCK, controllerMessenger), - ).toBe(3); - expect( - getRandomisedGasFeeDigits(CHAIN_ID_2_MOCK, controllerMessenger), - ).toBe(2); + expect(getGasFeeRandomisation(controllerMessenger)).toStrictEqual({ + randomisedGasFeeDigits: {}, + preservedNumberOfDigits: undefined, + }); }); - it('returns undefined if no randomised gas fee digits for chain', () => { + it('returns values from feature flags when set', () => { mockFeatureFlags({ [FEATURE_FLAG_TRANSACTIONS]: { - randomisedGasFeeDigits: { - [CHAIN_ID_2_MOCK]: 2, + gasFeeRandomisation: { + randomisedGasFeeDigits: { + [CHAIN_ID_MOCK]: 3, + [CHAIN_ID_2_MOCK]: 5, + }, + preservedNumberOfDigits: 2, }, }, }); - expect( - getRandomisedGasFeeDigits(CHAIN_ID_MOCK, controllerMessenger), - ).toBeUndefined(); - }); - - it('returns undefined if feature flag not configured', () => { - mockFeatureFlags({}); - expect( - getRandomisedGasFeeDigits(CHAIN_ID_MOCK, controllerMessenger), - ).toBeUndefined(); + expect(getGasFeeRandomisation(controllerMessenger)).toStrictEqual({ + randomisedGasFeeDigits: { + [CHAIN_ID_MOCK]: 3, + [CHAIN_ID_2_MOCK]: 5, + }, + preservedNumberOfDigits: 2, + }); }); - }); - describe('getPreserveNumberOfDigitsForRandomisedGasFee', () => { - it('returns value from remote feature flag controller', () => { + it('returns empty randomisedGasFeeDigits if not set in feature flags', () => { mockFeatureFlags({ [FEATURE_FLAG_TRANSACTIONS]: { - preservedNumberOfDigits: 5, + gasFeeRandomisation: { + preservedNumberOfDigits: 2, + }, }, }); - expect( - getPreserveNumberOfDigitsForRandomisedGasFee(controllerMessenger), - ).toBe(5); - }); - - it('returns undefined if feature flag not configured', () => { - mockFeatureFlags({}); - expect( - getPreserveNumberOfDigitsForRandomisedGasFee(controllerMessenger), - ).toBeUndefined(); + expect(getGasFeeRandomisation(controllerMessenger)).toStrictEqual({ + randomisedGasFeeDigits: {}, + preservedNumberOfDigits: 2, + }); }); }); }); diff --git a/packages/transaction-controller/src/utils/feature-flags.ts b/packages/transaction-controller/src/utils/feature-flags.ts index ac5c883991f..1082ba8e89b 100644 --- a/packages/transaction-controller/src/utils/feature-flags.ts +++ b/packages/transaction-controller/src/utils/feature-flags.ts @@ -64,11 +64,13 @@ export type TransactionControllerFeatureFlags = { defaultIntervalMs?: number; }; - /** Randomised gas fee digits. */ - randomisedGasFeeDigits?: Record; + gasFeeRandomisation?: { + /** Randomised gas fee digits per chainId. */ + randomisedGasFeeDigits?: Record; - /** Number of digits to preserve for randomised gas fee digits. */ - preservedNumberOfDigits?: number; + /** Number of digits to preserve for randomised gas fee digits. */ + preservedNumberOfDigits?: number; + }; }; }; @@ -181,39 +183,26 @@ export function getAcceleratedPollingParams( } /** - * Retrieves the number of digits to randomise for a given chain ID. - * - * @param chainId - The chain ID. - * @param messenger - The controller messenger instance. - * @returns The number of digits to randomise. - */ -export function getRandomisedGasFeeDigits( - chainId: Hex, - messenger: TransactionControllerMessenger, -): number | undefined { - const featureFlags = getFeatureFlags(messenger); - - const randomisedGasFeeDigits = - featureFlags?.[FEATURE_FLAG_TRANSACTIONS]?.randomisedGasFeeDigits ?? {}; - - return randomisedGasFeeDigits[chainId]; -} - -/** - * Retrieves the number of digits to preserve for randomised gas fee digits. + * Retrieves the gas fee randomisation parameters. * * @param messenger - The controller messenger instance. - * @returns The number of digits to preserve. + * @returns The gas fee randomisation parameters. */ -export function getPreserveNumberOfDigitsForRandomisedGasFee( +export function getGasFeeRandomisation( messenger: TransactionControllerMessenger, -): number | undefined { +): { + randomisedGasFeeDigits: Record; + preservedNumberOfDigits: number | undefined; +} { const featureFlags = getFeatureFlags(messenger); - const preservedNumberOfDigits = - featureFlags?.[FEATURE_FLAG_TRANSACTIONS]?.preservedNumberOfDigits; + const gasFeeRandomisation = + featureFlags?.[FEATURE_FLAG_TRANSACTIONS]?.gasFeeRandomisation || {}; - return preservedNumberOfDigits; + return { + randomisedGasFeeDigits: gasFeeRandomisation.randomisedGasFeeDigits || {}, + preservedNumberOfDigits: gasFeeRandomisation.preservedNumberOfDigits, + }; } /** From 041d95d4c83567c08c1b7a4c06a05b62709a101a Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 27 Mar 2025 14:11:03 +0100 Subject: [PATCH 23/24] Update --- .../RandomisedEstimationsGasFeeFlow.test.ts | 16 ++++++++-------- .../gas-flows/RandomisedEstimationsGasFeeFlow.ts | 15 +++++++-------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 5d580d85776..7763f56d1ea 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -353,7 +353,7 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { }); it('randomizes the last digits while preserving the significant digits', () => { - const result = randomiseDecimalGWEIAndConvertToHex('5', 3); + const result = randomiseDecimalGWEIAndConvertToHex('5', 3, 2); const resultWei = parseInt(result.slice(2), 16); const resultGwei = resultWei / 1e9; @@ -372,7 +372,7 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { jest.spyOn(global.Math, 'random').mockReturnValue(0); // Test with a value that has non-zero ending digits - const result = randomiseDecimalGWEIAndConvertToHex('5.000500123', 3); + const result = randomiseDecimalGWEIAndConvertToHex('5.000500123', 3, 2); const resultWei = parseInt(result.slice(2), 16); // Original value in Wei @@ -386,7 +386,7 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { // Set Math.random to return almost 1 jest.spyOn(global.Math, 'random').mockReturnValue(0.999); - const result = randomiseDecimalGWEIAndConvertToHex('5', 3); + const result = randomiseDecimalGWEIAndConvertToHex('5', 3, 2); const resultWei = parseInt(result.slice(2), 16); const baseWei = 5 * 1e9; @@ -398,7 +398,7 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { }); it('handles values with more digits than requested to randomize', () => { - const result = randomiseDecimalGWEIAndConvertToHex('1.23456789', 2); + const result = randomiseDecimalGWEIAndConvertToHex('1.23456789', 2, 2); const resultWei = parseInt(result.slice(2), 16); // Base should be 1.234567 Gwei in Wei @@ -415,7 +415,7 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { }); it('respects the PRESERVE_NUMBER_OF_DIGITS constant', () => { - const result = randomiseDecimalGWEIAndConvertToHex('0.00001', 4); + const result = randomiseDecimalGWEIAndConvertToHex('0.00001', 4, 2); const resultWei = parseInt(result.slice(2), 16); // Original value is 10000 Wei @@ -433,7 +433,7 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { // For "0" input, the result should still be 0 // This is because 0 has no "ending digits" to randomize // The implementation will still start from 0 and only randomize upward - const result = randomiseDecimalGWEIAndConvertToHex('0', 3); + const result = randomiseDecimalGWEIAndConvertToHex('0', 3, 2); const resultWei = parseInt(result.slice(2), 16); expect(resultWei).toBeGreaterThanOrEqual(0); @@ -441,8 +441,8 @@ describe('randomiseDecimalGWEIAndConvertToHex', () => { }); it('handles different number formats correctly', () => { - const resultFromNumber = randomiseDecimalGWEIAndConvertToHex(5, 3); - const resultFromString = randomiseDecimalGWEIAndConvertToHex('5', 3); + const resultFromNumber = randomiseDecimalGWEIAndConvertToHex(5, 3, 2); + const resultFromString = randomiseDecimalGWEIAndConvertToHex('5', 3, 2); expect(resultFromNumber).toStrictEqual(resultFromString); }); }); diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index f0b8000c545..44f3727f7ea 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -79,7 +79,9 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { const randomisedGasFeeDigits = gasFeeRandomisation.randomisedGasFeeDigits[transactionMeta.chainId]; - const preservedNumberOfDigits = gasFeeRandomisation.preservedNumberOfDigits; + const preservedNumberOfDigits = + gasFeeRandomisation.preservedNumberOfDigits ?? + DEFAULT_PRESERVE_NUMBER_OF_DIGITS; let response: GasFeeEstimates; @@ -111,7 +113,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { #getRandomisedFeeMarketEstimates( gasFeeEstimates: FeeMarketGasPriceEstimate, lastNDigits: number, - preservedNumberOfDigits?: number, + preservedNumberOfDigits: number, ): FeeMarketGasFeeEstimates { const levels = Object.values(GasFeeEstimateLevel).reduce( (result, level) => ({ @@ -136,7 +138,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { gasFeeEstimates: FeeMarketGasPriceEstimate, level: GasFeeEstimateLevel, lastNDigits: number, - preservedNumberOfDigits?: number, + preservedNumberOfDigits: number, ): FeeMarketGasFeeEstimateForLevel { return { maxFeePerGas: gweiDecimalToWeiHex( @@ -212,18 +214,15 @@ function generateRandomDigits(digitCount: number, minValue: number): number { export function randomiseDecimalGWEIAndConvertToHex( gweiDecimalValue: string | number, numberOfDigitsToRandomizeAtTheEnd: number, - preservedNumberOfDigits?: number, + preservedNumberOfDigits: number, ): Hex { const weiDecimalValue = gweiDecimalToWeiDecimal(gweiDecimalValue); const decimalLength = weiDecimalValue.length; - const preservedDigits = - preservedNumberOfDigits ?? DEFAULT_PRESERVE_NUMBER_OF_DIGITS; - // Determine how many digits to randomise while preserving the PRESERVE_NUMBER_OF_DIGITS const effectiveDigitsToRandomise = Math.min( numberOfDigitsToRandomizeAtTheEnd, - decimalLength - preservedDigits, + decimalLength - preservedNumberOfDigits, ); // Handle the case when the value is 0 or too small From 6a548b7c9ded74625ae50803a0e77af84c741263 Mon Sep 17 00:00:00 2001 From: OGPoyraz Date: Thu, 27 Mar 2025 15:16:07 +0100 Subject: [PATCH 24/24] Resolve default gas estimations if gasEstimateType is not market fee --- .../RandomisedEstimationsGasFeeFlow.test.ts | 65 +++++++------- .../RandomisedEstimationsGasFeeFlow.ts | 84 +++++-------------- 2 files changed, 56 insertions(+), 93 deletions(-) diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts index 7763f56d1ea..53bfdbe38e6 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.test.ts @@ -212,8 +212,23 @@ describe('RandomisedEstimationsGasFeeFlow', () => { ); it.each(Object.values(GasFeeEstimateLevel))( - 'does not randomise legacy estimates for %s level', + 'does return default legacy estimates for %s level', async (level) => { + const defaultLegacyEstimates = { + type: GasFeeEstimateType.Legacy, + [GasFeeEstimateLevel.Low]: toHex(1e9), + [GasFeeEstimateLevel.Medium]: toHex(3e9), + [GasFeeEstimateLevel.High]: toHex(5e9), + } as LegacyGasFeeEstimates; + + jest + .mocked(DefaultGasFeeFlow.prototype.getGasFees) + .mockImplementationOnce(async () => { + return { + estimates: defaultLegacyEstimates, + }; + }); + const flow = new RandomisedEstimationsGasFeeFlow(); const request = { @@ -221,33 +236,33 @@ describe('RandomisedEstimationsGasFeeFlow', () => { transactionMeta: TRANSACTION_META_MOCK, gasFeeControllerData: { gasEstimateType: GAS_ESTIMATE_TYPES.LEGACY, - gasFeeEstimates: { - low: '100000', - medium: '200000', - high: '300000', - }, } as GasFeeState, messenger: {} as TransactionControllerMessenger, }; const result = await flow.getGasFees(request); - // Verify result type expect(result.estimates.type).toBe(GasFeeEstimateType.Legacy); - - const gasHex = (result.estimates as LegacyGasFeeEstimates)[level]; - const estimates = request.gasFeeControllerData - .gasFeeEstimates as Record; - - // Verify that the gas price is not randomised - const originalValue = Number(estimates[level]); - const actualValue = parseInt(gasHex.slice(2), 16) / 1e9; - - expect(actualValue).toBe(originalValue); + expect((result.estimates as LegacyGasFeeEstimates)[level]).toBe( + defaultLegacyEstimates[level], + ); }, ); - it('does not randomise eth_gasPrice estimates', async () => { + it('does return default eth_gasPrice estimates', async () => { + const defaultGasPriceEstimates = { + type: GasFeeEstimateType.GasPrice, + gasPrice: toHex(200000), + } as GasPriceGasFeeEstimates; + + jest + .mocked(DefaultGasFeeFlow.prototype.getGasFees) + .mockImplementationOnce(async () => { + return { + estimates: defaultGasPriceEstimates, + }; + }); + const flow = new RandomisedEstimationsGasFeeFlow(); const request = { @@ -255,24 +270,16 @@ describe('RandomisedEstimationsGasFeeFlow', () => { transactionMeta: TRANSACTION_META_MOCK, gasFeeControllerData: { gasEstimateType: GAS_ESTIMATE_TYPES.ETH_GASPRICE, - gasFeeEstimates: { - gasPrice: '200000', - }, } as GasFeeState, messenger: {} as TransactionControllerMessenger, }; const result = await flow.getGasFees(request); - // Verify result type expect(result.estimates.type).toBe(GasFeeEstimateType.GasPrice); - - const gasHex = (result.estimates as GasPriceGasFeeEstimates).gasPrice; - const originalValue = 200000; - const actualValue = parseInt(gasHex.slice(2), 16) / 1e9; - - // Verify gas price is within expected range - expect(actualValue).toBe(originalValue); + expect((result.estimates as GasPriceGasFeeEstimates).gasPrice).toBe( + defaultGasPriceEstimates.gasPrice, + ); }); it('fall backs to default flow if randomization fails', async () => { diff --git a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts index 44f3727f7ea..b9b0391c2b6 100644 --- a/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts +++ b/packages/transaction-controller/src/gas-flows/RandomisedEstimationsGasFeeFlow.ts @@ -1,8 +1,4 @@ -import type { - LegacyGasPriceEstimate, - GasFeeEstimates as FeeMarketGasPriceEstimate, - EthGasPriceEstimate, -} from '@metamask/gas-fee-controller'; +import type { GasFeeEstimates as FeeMarketGasPriceEstimate } from '@metamask/gas-fee-controller'; import { GAS_ESTIMATE_TYPES } from '@metamask/gas-fee-controller'; import { add0x, createModuleLogger, type Hex } from '@metamask/utils'; @@ -12,12 +8,9 @@ import type { TransactionControllerMessenger } from '../TransactionController'; import type { FeeMarketGasFeeEstimateForLevel, FeeMarketGasFeeEstimates, - GasFeeEstimates, GasFeeFlow, GasFeeFlowRequest, GasFeeFlowResponse, - GasPriceGasFeeEstimates, - LegacyGasFeeEstimates, TransactionMeta, } from '../types'; import { GasFeeEstimateLevel, GasFeeEstimateType } from '../types'; @@ -57,7 +50,7 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { async getGasFees(request: GasFeeFlowRequest): Promise { try { - return this.#getRandomisedGasFees(request); + return await this.#getRandomisedGasFees(request); } catch (error) { log('Using default flow as fallback due to error', error); return await this.#getDefaultGasFees(request); @@ -70,7 +63,9 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { return new DefaultGasFeeFlow().getGasFees(request); } - #getRandomisedGasFees(request: GasFeeFlowRequest): GasFeeFlowResponse { + async #getRandomisedGasFees( + request: GasFeeFlowRequest, + ): Promise { const { messenger, gasFeeControllerData, transactionMeta } = request; const { gasEstimateType, gasFeeEstimates } = gasFeeControllerData; @@ -83,31 +78,25 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { gasFeeRandomisation.preservedNumberOfDigits ?? DEFAULT_PRESERVE_NUMBER_OF_DIGITS; - let response: GasFeeEstimates; - if (gasEstimateType === GAS_ESTIMATE_TYPES.FEE_MARKET) { - log('Using fee market estimates', gasFeeEstimates); - response = this.#getRandomisedFeeMarketEstimates( - gasFeeEstimates, - randomisedGasFeeDigits, - preservedNumberOfDigits, + log('Randomising fee market estimates', gasFeeEstimates); + const randomisedFeeMarketEstimates = + this.#getRandomisedFeeMarketEstimates( + gasFeeEstimates, + randomisedGasFeeDigits, + preservedNumberOfDigits, + ); + log( + 'Added randomised fee market estimates', + randomisedFeeMarketEstimates, ); - log('Added randomised fee market estimates', response); - } else if (gasEstimateType === GAS_ESTIMATE_TYPES.LEGACY) { - log('Using legacy estimates', gasFeeEstimates); - response = this.#getLegacyEstimates(gasFeeEstimates); - log('Added legacy estimates', response); - } else if (gasEstimateType === GAS_ESTIMATE_TYPES.ETH_GASPRICE) { - log('Using eth_gasPrice estimates', gasFeeEstimates); - response = this.#getGasPriceEstimates(gasFeeEstimates); - log('Added eth_gasPrice estimates', response); - } else { - throw new Error(`Unsupported gas estimate type: ${gasEstimateType}`); + + return { + estimates: randomisedFeeMarketEstimates, + }; } - return { - estimates: response, - }; + return await this.#getDefaultGasFees(request); } #getRandomisedFeeMarketEstimates( @@ -152,39 +141,6 @@ export class RandomisedEstimationsGasFeeFlow implements GasFeeFlow { ), }; } - - #getLegacyEstimates( - gasFeeEstimates: LegacyGasPriceEstimate, - ): LegacyGasFeeEstimates { - const levels = Object.values(GasFeeEstimateLevel).reduce( - (result, level) => ({ - ...result, - [level]: this.#getLegacyLevel(gasFeeEstimates, level), - }), - {} as Omit, - ); - - return { - type: GasFeeEstimateType.Legacy, - ...levels, - }; - } - - #getLegacyLevel( - gasFeeEstimates: LegacyGasPriceEstimate, - level: GasFeeEstimateLevel, - ): Hex { - return gweiDecimalToWeiHex(gasFeeEstimates[level]); - } - - #getGasPriceEstimates( - gasFeeEstimates: EthGasPriceEstimate, - ): GasPriceGasFeeEstimates { - return { - type: GasFeeEstimateType.GasPrice, - gasPrice: gweiDecimalToWeiHex(gasFeeEstimates.gasPrice), - }; - } } /**