diff --git a/app/scripts/controllers/preferences-controller-method-action-types.ts b/app/scripts/controllers/preferences-controller-method-action-types.ts index 9c55fd7a979a..11a0ecf6efa7 100644 --- a/app/scripts/controllers/preferences-controller-method-action-types.ts +++ b/app/scripts/controllers/preferences-controller-method-action-types.ts @@ -157,6 +157,7 @@ export type PreferencesControllerSetUseTransactionSimulationsAction = { * * @param options * @param options.chainId - The chainId the advancedGasFees should be set on + * @param options.account - The account the advancedGasFees should be set for * @param options.gasFeePreferences - The advancedGasFee options to set */ export type PreferencesControllerSetAdvancedGasFeeAction = { diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts index ab81ac86fb17..f79aed9821a6 100644 --- a/app/scripts/controllers/preferences-controller.test.ts +++ b/app/scripts/controllers/preferences-controller.test.ts @@ -265,25 +265,65 @@ describe('preferences controller', () => { describe('setAdvancedGasFee', () => { const { controller } = setupController({}); + const account = '0xabc'; + it('should default to an empty object', () => { expect(controller.state.advancedGasFee).toStrictEqual({}); }); it('should set the setAdvancedGasFee property in state', () => { controller.setAdvancedGasFee({ + account: '0xABC', chainId: CHAIN_IDS.GOERLI, gasFeePreferences: { + userFeeLevel: 'custom', maxBaseFee: '1.5', priorityFee: '2', }, }); expect( - controller.state.advancedGasFee[CHAIN_IDS.GOERLI].maxBaseFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI][account].userFeeLevel, + ).toStrictEqual('custom'); + expect( + controller.state.advancedGasFee[CHAIN_IDS.GOERLI][account].maxBaseFee, ).toStrictEqual('1.5'); expect( - controller.state.advancedGasFee[CHAIN_IDS.GOERLI].priorityFee, + controller.state.advancedGasFee[CHAIN_IDS.GOERLI][account].priorityFee, ).toStrictEqual('2'); }); + + it('should clear advancedGasFee for one account without clearing other accounts', () => { + const { controller: accountScopedController } = setupController({}); + accountScopedController.setAdvancedGasFee({ + account: '0xabc', + chainId: CHAIN_IDS.GOERLI, + gasFeePreferences: { + userFeeLevel: 'custom', + maxBaseFee: '1.5', + priorityFee: '2', + }, + }); + accountScopedController.setAdvancedGasFee({ + account: '0xdef', + chainId: CHAIN_IDS.GOERLI, + gasFeePreferences: { + userFeeLevel: 'high', + }, + }); + + accountScopedController.setAdvancedGasFee({ + account: '0xabc', + chainId: CHAIN_IDS.GOERLI, + }); + + expect( + accountScopedController.state.advancedGasFee[CHAIN_IDS.GOERLI], + ).toStrictEqual({ + '0xdef': { + userFeeLevel: 'high', + }, + }); + }); }); describe('setTheme', () => { @@ -1226,7 +1266,15 @@ describe('preferences controller', () => { currentLocale: 'ja', theme: ThemeType.dark, knownMethodData: { '0x12345678': 'transfer' }, - advancedGasFee: { '0x1': { maxBaseFee: '100', priorityFee: '10' } }, + advancedGasFee: { + '0x1': { + '0xabc': { + userFeeLevel: 'custom', + maxBaseFee: '100', + priorityFee: '10', + }, + }, + }, preferences: { autoLockTimeLimit: undefined, avatarType: 'jazzicon', diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts index 99175eea1bc8..74b0d2097ae0 100644 --- a/app/scripts/controllers/preferences-controller.ts +++ b/app/scripts/controllers/preferences-controller.ts @@ -17,6 +17,10 @@ import { DEFAULT_AUTO_LOCK_TIME_LIMIT, ThemeType, } from '../../../shared/constants/preferences'; +import type { + AdvancedGasFeePreferences, + AdvancedGasFeePreferencesByChain, +} from '../../../shared/constants/gas'; import { type DefaultAddressScope } from '../../../shared/constants/default-address'; import { DefiReferralPartner } from '../../../shared/constants/defi-referrals'; import { FALLBACK_LOCALE } from '../../../shared/lib/i18n'; @@ -96,7 +100,7 @@ export type PreferencesControllerState = Omit< | 'tokenNetworkFilter' > & { addSnapAccountEnabled?: boolean; - advancedGasFee: Record>; + advancedGasFee: AdvancedGasFeePreferencesByChain; currentLocale: string; dismissSeedBackUpReminder: boolean; enableMV3TimestampSave: boolean; @@ -697,20 +701,42 @@ export class PreferencesController extends BaseController< * * @param options * @param options.chainId - The chainId the advancedGasFees should be set on + * @param options.account - The account the advancedGasFees should be set for * @param options.gasFeePreferences - The advancedGasFee options to set */ setAdvancedGasFee({ + account, chainId, gasFeePreferences, }: { + account: string; chainId: string; - gasFeePreferences: Record; + gasFeePreferences?: AdvancedGasFeePreferences; }): void { - const { advancedGasFee } = this.state; this.update((state) => { + const normalizedAccount = account.toLowerCase(); + const chainPreferences = state.advancedGasFee[chainId] ?? {}; + + if (!gasFeePreferences) { + const { + [normalizedAccount]: _removedPreference, + ...remainingChainPreferences + } = chainPreferences; + + state.advancedGasFee = { + ...state.advancedGasFee, + [chainId]: remainingChainPreferences, + }; + + return; + } + state.advancedGasFee = { - ...advancedGasFee, - [chainId]: gasFeePreferences, + ...state.advancedGasFee, + [chainId]: { + ...chainPreferences, + [normalizedAccount]: gasFeePreferences, + }, }; }); } diff --git a/app/scripts/lib/transaction/metrics-builders/gas.test.ts b/app/scripts/lib/transaction/metrics-builders/gas.test.ts index 2fd00a2a0d21..5844fe6d7995 100644 --- a/app/scripts/lib/transaction/metrics-builders/gas.test.ts +++ b/app/scripts/lib/transaction/metrics-builders/gas.test.ts @@ -24,6 +24,7 @@ describe('gas builder', () => { }), ); expect(result.properties.gas_fee_selected).toBe('dapp_proposed'); + expect(result.properties.gas_fee_presented).toBe('medium'); expect(result.sensitiveProperties.max_fee_per_gas).toBe( hexWEIToDecGWEI('0x3b9aca00'), ); @@ -31,4 +32,21 @@ describe('gas builder', () => { hexWEIToDecGWEI('0x59682f00'), ); }); + + it('normalizes dapp suggested presented gas fee metrics', async () => { + const result = await getGasMetricsProperties( + createBuilderRequest({ + transactionMeta: { + ...createBuilderRequest().transactionMeta, + userFeeLevel: 'custom', + defaultGasEstimates: { + estimateType: 'dappSuggested', + }, + } as never, + }), + ); + + expect(result.properties.gas_fee_presented).toBe('dapp_proposed'); + expect(result.properties.gas_fee_selected).toBe('custom'); + }); }); diff --git a/app/scripts/lib/transaction/metrics-builders/gas.ts b/app/scripts/lib/transaction/metrics-builders/gas.ts index 6603099cb15c..7e0ed1df1468 100644 --- a/app/scripts/lib/transaction/metrics-builders/gas.ts +++ b/app/scripts/lib/transaction/metrics-builders/gas.ts @@ -13,10 +13,10 @@ export const getGasMetricsProperties: TransactionMetricsBuilder = async ({ transactionMeta, transactionMetricsRequest, }) => { - const gasFeeSelected = - transactionMeta.userFeeLevel === 'dappSuggested' - ? 'dapp_proposed' - : transactionMeta.userFeeLevel; + const gasFeeSelected = normalizeGasFeeLevel(transactionMeta.userFeeLevel); + const gasFeePresented = normalizeGasFeeLevel( + transactionMeta.defaultGasEstimates?.estimateType, + ); const gasParams: Record = {}; @@ -70,6 +70,7 @@ export const getGasMetricsProperties: TransactionMetricsBuilder = async ({ } return { properties: { + gas_fee_presented: gasFeePresented, gas_fee_selected: gasFeeSelected, }, sensitiveProperties: { @@ -85,3 +86,7 @@ export const getGasMetricsProperties: TransactionMetricsBuilder = async ({ }, }; }; + +function normalizeGasFeeLevel(userFeeLevel: string | undefined) { + return userFeeLevel === 'dappSuggested' ? 'dapp_proposed' : userFeeLevel; +} diff --git a/app/scripts/messenger-client-init/confirmations/transaction-controller-init.test.ts b/app/scripts/messenger-client-init/confirmations/transaction-controller-init.test.ts index ce243a4ef456..eb201ea43034 100644 --- a/app/scripts/messenger-client-init/confirmations/transaction-controller-init.test.ts +++ b/app/scripts/messenger-client-init/confirmations/transaction-controller-init.test.ts @@ -152,19 +152,56 @@ describe('Transaction Controller Init', () => { preferencesState: { advancedGasFee: { [CHAIN_ID_MOCK]: { - maxBaseFee: '0x1', - priorityFee: '0x2', + '0xabc': { + userFeeLevel: 'custom', + maxBaseFee: '0x1', + priorityFee: '0x2', + }, }, }, }, }); - expect(getSavedGasFees?.(CHAIN_ID_MOCK)).toStrictEqual({ + expect( + getSavedGasFees?.({ + chainId: CHAIN_ID_MOCK, + txParams: { + from: '0xABC', + }, + } as unknown as TransactionMeta), + ).toStrictEqual({ + level: 'custom', maxBaseFee: '0x1', priorityFee: '0x2', }); }); + it('does not retrieve saved gas fees for MetaMask Pay transactions', () => { + const getSavedGasFees = testConstructorOption('getSavedGasFees', { + preferencesState: { + advancedGasFee: { + [CHAIN_ID_MOCK]: { + '0xabc': { + userFeeLevel: 'custom', + maxBaseFee: '0x1', + priorityFee: '0x2', + }, + }, + }, + }, + }); + + expect( + getSavedGasFees?.({ + chainId: CHAIN_ID_MOCK, + metamaskPay: {}, + txParams: { + from: '0xabc', + }, + } as unknown as TransactionMeta), + ).toBeUndefined(); + }); + describe('determines incoming transactions is disabled', () => { it('when useExternalServices is enabled in preferences and onboarding complete', () => { const incomingTransactionsIsEnabled = testConstructorOption( diff --git a/app/scripts/messenger-client-init/confirmations/transaction-controller-init.ts b/app/scripts/messenger-client-init/confirmations/transaction-controller-init.ts index 457c49f70aa7..e1773a9b19b2 100644 --- a/app/scripts/messenger-client-init/confirmations/transaction-controller-init.ts +++ b/app/scripts/messenger-client-init/confirmations/transaction-controller-init.ts @@ -1,11 +1,13 @@ import { PRODUCT_TYPES } from '@metamask/subscription-controller'; import { ORIGIN_METAMASK } from '@metamask/controller-utils'; import { + GasFeeEstimateLevel, SavedGasFees, TransactionController, TransactionControllerMessenger, TransactionMeta, TransactionType, + UserFeeLevel, } from '@metamask/transaction-controller'; import { Hex } from '@metamask/utils'; import { trace } from '../../../../shared/lib/trace'; @@ -203,10 +205,47 @@ function addTransactionControllerListeners( function getSavedGasFees( { messenger }: { messenger: TransactionControllerInitMessenger }, - chainId: string, + transactionMeta: TransactionMeta, ): SavedGasFees | undefined { + const account = transactionMeta.txParams.from?.toLowerCase(); + + if (!account || transactionMeta.metamaskPay) { + return undefined; + } + const { advancedGasFee } = messenger.call('PreferencesController:getState'); - return advancedGasFee[chainId] as unknown as SavedGasFees | undefined; + const savedGasFeePreference = + advancedGasFee[transactionMeta.chainId]?.[account]; + + if (!savedGasFeePreference) { + return undefined; + } + + const savedGasFeeLevel = getSavedGasFeeLevel( + savedGasFeePreference.userFeeLevel, + ); + + if (!savedGasFeeLevel) { + return undefined; + } + + const savedGasFees: SavedGasFees = { + level: savedGasFeeLevel, + }; + + if (savedGasFeePreference.maxBaseFee) { + savedGasFees.maxBaseFee = savedGasFeePreference.maxBaseFee; + } + + if (savedGasFeePreference.priorityFee) { + savedGasFees.priorityFee = savedGasFeePreference.priorityFee; + } + + if (savedGasFeePreference.gasPrice) { + savedGasFees.gasPrice = savedGasFeePreference.gasPrice; + } + + return savedGasFees; } async function getSimulationConfig( @@ -289,3 +328,16 @@ function isAutomaticGasFeeUpdateEnabled(transaction: TransactionMeta) { DISABLED_AUTOMATIC_GAS_FEE_UPDATE_TYPES, ); } + +function getSavedGasFeeLevel( + userFeeLevel: string, +): SavedGasFees['level'] | undefined { + const savedGasFeeLevels = [ + GasFeeEstimateLevel.Low, + GasFeeEstimateLevel.Medium, + GasFeeEstimateLevel.High, + UserFeeLevel.CUSTOM, + ] as const; + + return savedGasFeeLevels.find((level) => level === userFeeLevel); +} diff --git a/app/scripts/migrations/214.test.ts b/app/scripts/migrations/214.test.ts new file mode 100644 index 000000000000..9db6759c979a --- /dev/null +++ b/app/scripts/migrations/214.test.ts @@ -0,0 +1,95 @@ +import { migrate, version } from './214'; + +const oldVersion = version - 1; + +describe(`migration #${version}`, () => { + it('bumps the state version', async () => { + const state = { meta: { version: oldVersion }, data: {} }; + await migrate(state, new Set()); + expect(state.meta.version).toBe(version); + }); + + it('is a no-op when PreferencesController state is missing', async () => { + const state = { meta: { version: oldVersion }, data: {} }; + const changed = new Set(); + await migrate(state, changed); + expect(state.data).toEqual({}); + expect(changed.size).toBe(0); + }); + + it('resets chain-scoped advanced gas fee preferences', async () => { + const state = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + advancedGasFee: { + '0x1': { + maxBaseFee: '75', + priorityFee: '2', + }, + }, + }, + }, + }; + const changed = new Set(); + await migrate(state, changed); + + expect(state.data.PreferencesController.advancedGasFee).toStrictEqual({}); + expect(changed.has('PreferencesController')).toBe(true); + }); + + it('preserves account-scoped advanced gas fee preferences', async () => { + const state = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + advancedGasFee: { + '0x1': { + '0xABC': { + userFeeLevel: 'custom', + maxBaseFee: '75', + priorityFee: '2', + }, + }, + }, + }, + }, + }; + const changed = new Set(); + await migrate(state, changed); + + expect(state.data.PreferencesController.advancedGasFee).toStrictEqual({ + '0x1': { + '0xabc': { + userFeeLevel: 'custom', + maxBaseFee: '75', + priorityFee: '2', + }, + }, + }); + expect(changed.has('PreferencesController')).toBe(true); + }); + + it('removes malformed account-scoped advanced gas fee preferences', async () => { + const state = { + meta: { version: oldVersion }, + data: { + PreferencesController: { + advancedGasFee: { + '0x1': { + '0xabc': { + userFeeLevel: 'custom', + maxBaseFee: 75, + }, + }, + }, + }, + }, + }; + const changed = new Set(); + await migrate(state, changed); + + expect(state.data.PreferencesController.advancedGasFee).toStrictEqual({}); + expect(changed.has('PreferencesController')).toBe(true); + }); +}); diff --git a/app/scripts/migrations/214.ts b/app/scripts/migrations/214.ts new file mode 100644 index 000000000000..89c05e773ee0 --- /dev/null +++ b/app/scripts/migrations/214.ts @@ -0,0 +1,112 @@ +import { getErrorMessage, hasProperty, isObject } from '@metamask/utils'; +import { captureException } from '../../../shared/lib/sentry'; +import type { Migrate } from './types'; + +export const version = 214; + +/** + * Migration 214: update `PreferencesController.advancedGasFee` from + * chain-scoped values to chain-and-account-scoped values. + * + * Migration 092.3 already wiped the original global advanced gas fee values, + * but it intentionally left any chain-scoped data untouched because that shape + * did not match the global data being removed. Since the new implementation + * requires account-scoped data under each chain, this migration defensively + * discards any remaining chain-scoped values that cannot be safely assigned to + * a single account. + * + * @param versionedData - The versioned data object to migrate. + * @param changedControllers - A set used to record controllers that were modified. + */ +export const migrate = (async (versionedData, changedControllers) => { + versionedData.meta.version = version; + try { + transformState(versionedData.data, changedControllers); + } catch (error) { + captureException( + new Error(`Migration #${version}: ${getErrorMessage(error)}`), + ); + } +}) satisfies Migrate; + +function transformState( + state: Record, + changedControllers: Set, +): void { + if ( + !hasProperty(state, 'PreferencesController') || + !isObject(state.PreferencesController) + ) { + return; + } + + const { PreferencesController } = state; + + if (!hasProperty(PreferencesController, 'advancedGasFee')) { + return; + } + + const { advancedGasFee } = PreferencesController; + + if (!isObject(advancedGasFee)) { + PreferencesController.advancedGasFee = {}; + changedControllers.add('PreferencesController'); + return; + } + + const migratedAdvancedGasFee: Record> = {}; + let didChange = false; + + for (const [chainId, chainPreferences] of Object.entries(advancedGasFee)) { + if ( + !isObject(chainPreferences) || + isLegacyAdvancedGasFeePreference(chainPreferences) + ) { + didChange = true; + continue; + } + + const migratedChainPreferences: Record = {}; + + for (const [account, preference] of Object.entries(chainPreferences)) { + if (!isAdvancedGasFeePreference(preference)) { + didChange = true; + continue; + } + + const normalizedAccount = account.toLowerCase(); + migratedChainPreferences[normalizedAccount] = preference; + didChange ||= normalizedAccount !== account; + } + + if (Object.keys(migratedChainPreferences).length > 0) { + migratedAdvancedGasFee[chainId] = migratedChainPreferences; + } + } + + if (!didChange) { + return; + } + + PreferencesController.advancedGasFee = migratedAdvancedGasFee; + changedControllers.add('PreferencesController'); +} + +function isLegacyAdvancedGasFeePreference(value: Record) { + return ( + hasProperty(value, 'maxBaseFee') || + hasProperty(value, 'priorityFee') || + hasProperty(value, 'gasPrice') + ); +} + +function isAdvancedGasFeePreference(value: unknown): boolean { + if (!isObject(value) || typeof value.userFeeLevel !== 'string') { + return false; + } + + return ['maxBaseFee', 'priorityFee', 'gasPrice'].every((key) => { + const preferenceValue = value[key]; + return preferenceValue === undefined || typeof preferenceValue === 'string'; + }); +} diff --git a/app/scripts/migrations/index.js b/app/scripts/migrations/index.js index 53d7bf3dc0a9..27798ff03f1f 100644 --- a/app/scripts/migrations/index.js +++ b/app/scripts/migrations/index.js @@ -249,6 +249,7 @@ const migrations = [ require('./211'), require('./212'), require('./213'), + require('./214'), ]; export default migrations; diff --git a/lavamoat/browserify/beta/policy.json b/lavamoat/browserify/beta/policy.json index d19d2cc59f39..2dfcc04c1fcf 100644 --- a/lavamoat/browserify/beta/policy.json +++ b/lavamoat/browserify/beta/policy.json @@ -943,7 +943,7 @@ "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, - "@metamask/keyring-controller": true, + "@metamask/accounts-controller>@metamask/keyring-controller": true, "@metamask/keyring-sdk": true, "@metamask/keyring-utils": true, "@metamask/superstruct": true, @@ -1204,6 +1204,7 @@ "setTimeout": true }, "packages": { + "@ethersproject/abi": true, "@metamask/controller-utils>@metamask/ethjs-unit": true, "@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, @@ -1926,6 +1927,23 @@ "@metamask/keyring-controller>ulid": true } }, + "@metamask/accounts-controller>@metamask/keyring-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/base-controller": true, + "@metamask/eth-hd-keyring": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/utils": true, + "async-mutex": true, + "@metamask/keyring-controller>ethereumjs-wallet": true, + "lodash": true, + "@metamask/keyring-controller>ulid": true + } + }, "@metamask/eip-5792-middleware>@metamask/transaction-controller>@metamask/accounts-controller>@metamask/keyring-controller": { "globals": { "console.error": true @@ -2753,9 +2771,11 @@ }, "@metamask/transaction-controller": { "globals": { + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/browserify/experimental/policy.json b/lavamoat/browserify/experimental/policy.json index d19d2cc59f39..2dfcc04c1fcf 100644 --- a/lavamoat/browserify/experimental/policy.json +++ b/lavamoat/browserify/experimental/policy.json @@ -943,7 +943,7 @@ "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, - "@metamask/keyring-controller": true, + "@metamask/accounts-controller>@metamask/keyring-controller": true, "@metamask/keyring-sdk": true, "@metamask/keyring-utils": true, "@metamask/superstruct": true, @@ -1204,6 +1204,7 @@ "setTimeout": true }, "packages": { + "@ethersproject/abi": true, "@metamask/controller-utils>@metamask/ethjs-unit": true, "@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, @@ -1926,6 +1927,23 @@ "@metamask/keyring-controller>ulid": true } }, + "@metamask/accounts-controller>@metamask/keyring-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/base-controller": true, + "@metamask/eth-hd-keyring": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/utils": true, + "async-mutex": true, + "@metamask/keyring-controller>ethereumjs-wallet": true, + "lodash": true, + "@metamask/keyring-controller>ulid": true + } + }, "@metamask/eip-5792-middleware>@metamask/transaction-controller>@metamask/accounts-controller>@metamask/keyring-controller": { "globals": { "console.error": true @@ -2753,9 +2771,11 @@ }, "@metamask/transaction-controller": { "globals": { + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/browserify/flask/policy.json b/lavamoat/browserify/flask/policy.json index d19d2cc59f39..2dfcc04c1fcf 100644 --- a/lavamoat/browserify/flask/policy.json +++ b/lavamoat/browserify/flask/policy.json @@ -943,7 +943,7 @@ "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, - "@metamask/keyring-controller": true, + "@metamask/accounts-controller>@metamask/keyring-controller": true, "@metamask/keyring-sdk": true, "@metamask/keyring-utils": true, "@metamask/superstruct": true, @@ -1204,6 +1204,7 @@ "setTimeout": true }, "packages": { + "@ethersproject/abi": true, "@metamask/controller-utils>@metamask/ethjs-unit": true, "@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, @@ -1926,6 +1927,23 @@ "@metamask/keyring-controller>ulid": true } }, + "@metamask/accounts-controller>@metamask/keyring-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/base-controller": true, + "@metamask/eth-hd-keyring": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/utils": true, + "async-mutex": true, + "@metamask/keyring-controller>ethereumjs-wallet": true, + "lodash": true, + "@metamask/keyring-controller>ulid": true + } + }, "@metamask/eip-5792-middleware>@metamask/transaction-controller>@metamask/accounts-controller>@metamask/keyring-controller": { "globals": { "console.error": true @@ -2753,9 +2771,11 @@ }, "@metamask/transaction-controller": { "globals": { + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/browserify/main/policy.json b/lavamoat/browserify/main/policy.json index d19d2cc59f39..2dfcc04c1fcf 100644 --- a/lavamoat/browserify/main/policy.json +++ b/lavamoat/browserify/main/policy.json @@ -943,7 +943,7 @@ "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, - "@metamask/keyring-controller": true, + "@metamask/accounts-controller>@metamask/keyring-controller": true, "@metamask/keyring-sdk": true, "@metamask/keyring-utils": true, "@metamask/superstruct": true, @@ -1204,6 +1204,7 @@ "setTimeout": true }, "packages": { + "@ethersproject/abi": true, "@metamask/controller-utils>@metamask/ethjs-unit": true, "@metamask/utils": true, "@metamask/controller-utils>@spruceid/siwe-parser": true, @@ -1926,6 +1927,23 @@ "@metamask/keyring-controller>ulid": true } }, + "@metamask/accounts-controller>@metamask/keyring-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/base-controller": true, + "@metamask/eth-hd-keyring": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/utils": true, + "async-mutex": true, + "@metamask/keyring-controller>ethereumjs-wallet": true, + "lodash": true, + "@metamask/keyring-controller>ulid": true + } + }, "@metamask/eip-5792-middleware>@metamask/transaction-controller>@metamask/accounts-controller>@metamask/keyring-controller": { "globals": { "console.error": true @@ -2753,9 +2771,11 @@ }, "@metamask/transaction-controller": { "globals": { + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/webpack/mv2/beta/policy.json b/lavamoat/webpack/mv2/beta/policy.json index 4c4e40cd0312..5984d2a2c37f 100644 --- a/lavamoat/webpack/mv2/beta/policy.json +++ b/lavamoat/webpack/mv2/beta/policy.json @@ -836,7 +836,7 @@ "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, - "@metamask/keyring-controller": true, + "@metamask/accounts-controller>@metamask/keyring-controller": true, "@metamask/keyring-sdk": true, "@metamask/keyring-utils": true, "@metamask/superstruct": true, @@ -1797,6 +1797,23 @@ "@metamask/keyring-controller>ulid": true } }, + "@metamask/accounts-controller>@metamask/keyring-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/base-controller": true, + "@metamask/eth-hd-keyring": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/utils": true, + "async-mutex": true, + "@metamask/keyring-controller>ethereumjs-wallet": true, + "lodash": true, + "@metamask/keyring-controller>ulid": true + } + }, "@metamask/eip-5792-middleware>@metamask/transaction-controller>@metamask/accounts-controller>@metamask/keyring-controller": { "globals": { "console.error": true @@ -2603,9 +2620,11 @@ "@metamask/transaction-controller": { "globals": { "Buffer.from": true, + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/webpack/mv2/experimental/policy.json b/lavamoat/webpack/mv2/experimental/policy.json index 4c4e40cd0312..5984d2a2c37f 100644 --- a/lavamoat/webpack/mv2/experimental/policy.json +++ b/lavamoat/webpack/mv2/experimental/policy.json @@ -836,7 +836,7 @@ "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, - "@metamask/keyring-controller": true, + "@metamask/accounts-controller>@metamask/keyring-controller": true, "@metamask/keyring-sdk": true, "@metamask/keyring-utils": true, "@metamask/superstruct": true, @@ -1797,6 +1797,23 @@ "@metamask/keyring-controller>ulid": true } }, + "@metamask/accounts-controller>@metamask/keyring-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/base-controller": true, + "@metamask/eth-hd-keyring": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/utils": true, + "async-mutex": true, + "@metamask/keyring-controller>ethereumjs-wallet": true, + "lodash": true, + "@metamask/keyring-controller>ulid": true + } + }, "@metamask/eip-5792-middleware>@metamask/transaction-controller>@metamask/accounts-controller>@metamask/keyring-controller": { "globals": { "console.error": true @@ -2603,9 +2620,11 @@ "@metamask/transaction-controller": { "globals": { "Buffer.from": true, + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/webpack/mv2/flask/policy.json b/lavamoat/webpack/mv2/flask/policy.json index 4c4e40cd0312..5984d2a2c37f 100644 --- a/lavamoat/webpack/mv2/flask/policy.json +++ b/lavamoat/webpack/mv2/flask/policy.json @@ -836,7 +836,7 @@ "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, - "@metamask/keyring-controller": true, + "@metamask/accounts-controller>@metamask/keyring-controller": true, "@metamask/keyring-sdk": true, "@metamask/keyring-utils": true, "@metamask/superstruct": true, @@ -1797,6 +1797,23 @@ "@metamask/keyring-controller>ulid": true } }, + "@metamask/accounts-controller>@metamask/keyring-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/base-controller": true, + "@metamask/eth-hd-keyring": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/utils": true, + "async-mutex": true, + "@metamask/keyring-controller>ethereumjs-wallet": true, + "lodash": true, + "@metamask/keyring-controller>ulid": true + } + }, "@metamask/eip-5792-middleware>@metamask/transaction-controller>@metamask/accounts-controller>@metamask/keyring-controller": { "globals": { "console.error": true @@ -2603,9 +2620,11 @@ "@metamask/transaction-controller": { "globals": { "Buffer.from": true, + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/webpack/mv2/main/policy.json b/lavamoat/webpack/mv2/main/policy.json index 4c4e40cd0312..5984d2a2c37f 100644 --- a/lavamoat/webpack/mv2/main/policy.json +++ b/lavamoat/webpack/mv2/main/policy.json @@ -836,7 +836,7 @@ "@metamask/base-controller": true, "@metamask/eth-snap-keyring": true, "@metamask/keyring-api": true, - "@metamask/keyring-controller": true, + "@metamask/accounts-controller>@metamask/keyring-controller": true, "@metamask/keyring-sdk": true, "@metamask/keyring-utils": true, "@metamask/superstruct": true, @@ -1797,6 +1797,23 @@ "@metamask/keyring-controller>ulid": true } }, + "@metamask/accounts-controller>@metamask/keyring-controller": { + "globals": { + "console.error": true + }, + "packages": { + "@ethereumjs/tx>@ethereumjs/util": true, + "@metamask/base-controller": true, + "@metamask/eth-hd-keyring": true, + "@metamask/eth-sig-util": true, + "@metamask/keyring-controller>@metamask/eth-simple-keyring": true, + "@metamask/utils": true, + "async-mutex": true, + "@metamask/keyring-controller>ethereumjs-wallet": true, + "lodash": true, + "@metamask/keyring-controller>ulid": true + } + }, "@metamask/eip-5792-middleware>@metamask/transaction-controller>@metamask/accounts-controller>@metamask/keyring-controller": { "globals": { "console.error": true @@ -2603,9 +2620,11 @@ "@metamask/transaction-controller": { "globals": { "Buffer.from": true, + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/webpack/mv3/beta/policy.json b/lavamoat/webpack/mv3/beta/policy.json index b4893e7d7995..979c538906f5 100644 --- a/lavamoat/webpack/mv3/beta/policy.json +++ b/lavamoat/webpack/mv3/beta/policy.json @@ -1489,9 +1489,11 @@ "@metamask/transaction-controller": { "globals": { "Buffer.from": true, + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/webpack/mv3/experimental/policy.json b/lavamoat/webpack/mv3/experimental/policy.json index b4893e7d7995..979c538906f5 100644 --- a/lavamoat/webpack/mv3/experimental/policy.json +++ b/lavamoat/webpack/mv3/experimental/policy.json @@ -1489,9 +1489,11 @@ "@metamask/transaction-controller": { "globals": { "Buffer.from": true, + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/webpack/mv3/flask/policy.json b/lavamoat/webpack/mv3/flask/policy.json index b4893e7d7995..979c538906f5 100644 --- a/lavamoat/webpack/mv3/flask/policy.json +++ b/lavamoat/webpack/mv3/flask/policy.json @@ -1489,9 +1489,11 @@ "@metamask/transaction-controller": { "globals": { "Buffer.from": true, + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/lavamoat/webpack/mv3/main/policy.json b/lavamoat/webpack/mv3/main/policy.json index b4893e7d7995..979c538906f5 100644 --- a/lavamoat/webpack/mv3/main/policy.json +++ b/lavamoat/webpack/mv3/main/policy.json @@ -1489,9 +1489,11 @@ "@metamask/transaction-controller": { "globals": { "Buffer.from": true, + "clearInterval": true, "clearTimeout": true, "console.error": true, "fetch": true, + "setInterval": true, "setTimeout": true }, "packages": { diff --git a/package.json b/package.json index 2e69ff7502ca..c772e408684e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,12 @@ "type": "git", "url": "https://github.com/MetaMask/metamask-extension.git" }, + "previewBuilds": { + "@metamask/transaction-controller": { + "type": "breaking", + "previewVersion": "68.0.0-preview-1a8241c" + } + }, "scripts": { "webpack": "tsx ./development/webpack/launch.ts", "webpack:clearcache": "./development/clear-webpack-cache.js", diff --git a/shared/constants/gas.ts b/shared/constants/gas.ts index 4586d741bce4..6fa569c08c4c 100644 --- a/shared/constants/gas.ts +++ b/shared/constants/gas.ts @@ -154,3 +154,15 @@ export type TxGasFees = { /** Estimate level user selected. */ userFeeLevel: string; }; + +export type AdvancedGasFeePreferences = { + userFeeLevel: string; + maxBaseFee?: string; + priorityFee?: string; + gasPrice?: string; +}; + +export type AdvancedGasFeePreferencesByChain = Record< + string, + Record +>; diff --git a/test/data/mock-send-state.json b/test/data/mock-send-state.json index 0cc5970f4937..c774568bf79b 100644 --- a/test/data/mock-send-state.json +++ b/test/data/mock-send-state.json @@ -462,10 +462,7 @@ "unapprovedTypedMessages": {}, "unapprovedTypedMessagesCount": 0, "useTokenDetection": true, - "advancedGasFee": { - "maxBaseFee": "75", - "priorityFee": "2" - }, + "advancedGasFee": {}, "tokenList": { "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599": { "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", diff --git a/test/data/mock-state.json b/test/data/mock-state.json index f45247f586ae..fb48b1604353 100644 --- a/test/data/mock-state.json +++ b/test/data/mock-state.json @@ -1090,8 +1090,11 @@ "hiddenAccountList": [], "advancedGasFee": { "0x5": { - "maxBaseFee": "75", - "priorityFee": "2" + "0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc": { + "userFeeLevel": "custom", + "maxBaseFee": "75", + "priorityFee": "2" + } } }, "nftsDropdownState": { diff --git a/test/e2e/fixtures/default-fixture.json b/test/e2e/fixtures/default-fixture.json index 4472c522c52a..402d45b98c3a 100644 --- a/test/e2e/fixtures/default-fixture.json +++ b/test/e2e/fixtures/default-fixture.json @@ -174,7 +174,7 @@ "announcements": {} }, "AppMetadataController": { - "currentMigrationVersion": 213, + "currentMigrationVersion": 214, "firstTimeInfo": {}, "previousAppVersion": "", "previousMigrationVersion": 0 @@ -1260,6 +1260,6 @@ }, "meta": { "storageKind": "split", - "version": 213 + "version": 214 } } diff --git a/test/e2e/fixtures/onboarding-fixture.json b/test/e2e/fixtures/onboarding-fixture.json index 3b22ac6b9aee..a470470c9b6f 100644 --- a/test/e2e/fixtures/onboarding-fixture.json +++ b/test/e2e/fixtures/onboarding-fixture.json @@ -35,7 +35,7 @@ "announcements": {} }, "AppMetadataController": { - "currentMigrationVersion": 213, + "currentMigrationVersion": 214, "firstTimeInfo": { "date": 0, "version": "13.17.0" @@ -2274,6 +2274,6 @@ }, "meta": { "storageKind": "split", - "version": 213 + "version": 214 } } diff --git a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json index 3fdd0c84eaf0..069865404a13 100644 --- a/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json +++ b/test/e2e/tests/metrics/state-snapshots/errors-before-init-opt-in-ui-state.json @@ -299,5 +299,5 @@ }, "config": "object" }, - "meta": { "storageKind": "split", "version": 213 } + "meta": { "storageKind": "split", "version": 214 } } diff --git a/test/integration/data/integration-init-state.json b/test/integration/data/integration-init-state.json index 5a02bd24e54c..7cbc48a5a858 100644 --- a/test/integration/data/integration-init-state.json +++ b/test/integration/data/integration-init-state.json @@ -88,8 +88,11 @@ "addressSecurityAlertResponses": {}, "advancedGasFee": { "0xaa36a7": { - "maxBaseFee": "75", - "priorityFee": "2" + "0xc42edfcc21ed14dda456aa0756c153f7985d8813": { + "userFeeLevel": "custom", + "maxBaseFee": "75", + "priorityFee": "2" + } } }, "alertEnabledness": { diff --git a/ui/pages/confirmations/components/modals/advanced-eip1559-modal/advanced-eip1559-modal.test.tsx b/ui/pages/confirmations/components/modals/advanced-eip1559-modal/advanced-eip1559-modal.test.tsx index fc9a4231141d..1e6a7ce5eac3 100644 --- a/ui/pages/confirmations/components/modals/advanced-eip1559-modal/advanced-eip1559-modal.test.tsx +++ b/ui/pages/confirmations/components/modals/advanced-eip1559-modal/advanced-eip1559-modal.test.tsx @@ -1,6 +1,9 @@ import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { + CHAIN_IDS, + type TransactionMeta, +} from '@metamask/transaction-controller'; import configureStore from '../../../../../store/store'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import { getMockConfirmStateForTransaction } from '../../../../../../test/data/confirmations/helper'; @@ -9,6 +12,8 @@ import { GasModalType } from '../../../constants/gas'; import { enLocale as messages } from '../../../../../../test/lib/i18n-helpers'; import { AdvancedEIP1559Modal } from './advanced-eip1559-modal'; +const mockPersistGasFeePreference = jest.fn(); + jest.mock('../../max-base-fee-input/max-base-fee-input', () => ({ MaxBaseFeeInput: () => (
Max Base Fee Input
@@ -25,10 +30,22 @@ jest.mock('../../gas-input/gas-input', () => ({ GasInput: () =>
Gas Input
, })); +jest.mock('../../../hooks/gas/usePersistGasFeePreference', () => ({ + usePersistGasFeePreference: () => mockPersistGasFeePreference, +})); + +jest.mock('../../../../../store/actions/update-transaction-gas-fees', () => ({ + updateTransactionGasFees: jest.fn(() => ({ type: 'update-gas-fees' })), +})); + const render = () => { const contractInteraction = genUnapprovedContractInteractionConfirmation({ chainId: CHAIN_IDS.GOERLI, - }); + }) as TransactionMeta; + contractInteraction.txParams.from = + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; + contractInteraction.txParams.maxFeePerGas = '0x3b9aca00'; + contractInteraction.txParams.maxPriorityFeePerGas = '0x59682f00'; const store = configureStore( getMockConfirmStateForTransaction(contractInteraction), @@ -47,6 +64,7 @@ const render = () => { return { ...result, + contractInteraction, mockSetActiveModal, mockHandleCloseModals, }; @@ -88,4 +106,22 @@ describe('AdvancedEIP1559Modal', () => { GasModalType.EstimatesModal, ); }); + + it('persists custom EIP-1559 gas fee preferences when Save is clicked', async () => { + const { contractInteraction, getByText, mockHandleCloseModals } = render(); + + fireEvent.click(getByText(messages.save.message)); + + await waitFor(() => { + expect(mockPersistGasFeePreference).toHaveBeenCalledWith( + contractInteraction, + { + userFeeLevel: 'custom', + maxBaseFee: '1', + priorityFee: '1.5', + }, + ); + }); + expect(mockHandleCloseModals).toHaveBeenCalledTimes(1); + }); }); diff --git a/ui/pages/confirmations/components/modals/advanced-eip1559-modal/advanced-eip1559-modal.tsx b/ui/pages/confirmations/components/modals/advanced-eip1559-modal/advanced-eip1559-modal.tsx index bf8b76585b90..add77017ae63 100644 --- a/ui/pages/confirmations/components/modals/advanced-eip1559-modal/advanced-eip1559-modal.tsx +++ b/ui/pages/confirmations/components/modals/advanced-eip1559-modal/advanced-eip1559-modal.tsx @@ -31,6 +31,8 @@ import { GasInput } from '../../gas-input/gas-input'; import { useConfirmContext } from '../../../context/confirm'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { updateTransactionGasFees } from '../../../../../store/actions/update-transaction-gas-fees'; +import { hexWEIToDecGWEI } from '../../../../../../shared/lib/conversion.utils'; +import { usePersistGasFeePreference } from '../../../hooks/gas/usePersistGasFeePreference'; export const AdvancedEIP1559Modal = ({ setActiveModal, @@ -41,6 +43,7 @@ export const AdvancedEIP1559Modal = ({ }) => { const t = useI18nContext(); const dispatch = useDispatch(); + const persistGasFeePreference = usePersistGasFeePreference(); const { currentConfirmation: transactionMeta } = useConfirmContext(); @@ -70,18 +73,29 @@ export const AdvancedEIP1559Modal = ({ errors.gas || errors.maxFeePerGas || errors.maxPriorityFeePerGas, ); - const handleSaveClick = useCallback(() => { + const handleSaveClick = useCallback(async () => { if (!transactionMeta?.id) { return; } - dispatch( + await dispatch( updateTransactionGasFees(transactionMeta.id, { userFeeLevel: UserFeeLevel.CUSTOM, ...pickBy(gasParams, Boolean), }), ); + await persistGasFeePreference(transactionMeta, { + userFeeLevel: UserFeeLevel.CUSTOM, + maxBaseFee: hexWEIToDecGWEI(gasParams.maxFeePerGas), + priorityFee: hexWEIToDecGWEI(gasParams.maxPriorityFeePerGas), + }); handleCloseModals(); - }, [transactionMeta?.id, gasParams, handleCloseModals, dispatch]); + }, [ + transactionMeta, + gasParams, + handleCloseModals, + dispatch, + persistGasFeePreference, + ]); const navigateToEstimatesModal = useCallback(() => { setActiveModal(GasModalType.EstimatesModal); diff --git a/ui/pages/confirmations/components/modals/advanced-gas-price-modal/advanced-gas-price-modal.test.tsx b/ui/pages/confirmations/components/modals/advanced-gas-price-modal/advanced-gas-price-modal.test.tsx index 28eb221005b6..7c1cc9419bd5 100644 --- a/ui/pages/confirmations/components/modals/advanced-gas-price-modal/advanced-gas-price-modal.test.tsx +++ b/ui/pages/confirmations/components/modals/advanced-gas-price-modal/advanced-gas-price-modal.test.tsx @@ -1,6 +1,9 @@ import React from 'react'; -import { fireEvent } from '@testing-library/react'; -import { CHAIN_IDS } from '@metamask/transaction-controller'; +import { fireEvent, waitFor } from '@testing-library/react'; +import { + CHAIN_IDS, + type TransactionMeta, +} from '@metamask/transaction-controller'; import configureStore from '../../../../../store/store'; import { renderWithConfirmContextProvider } from '../../../../../../test/lib/confirmations/render-helpers'; import { getMockConfirmStateForTransaction } from '../../../../../../test/data/confirmations/helper'; @@ -9,6 +12,8 @@ import { GasModalType } from '../../../constants/gas'; import { enLocale as messages } from '../../../../../../test/lib/i18n-helpers'; import { AdvancedGasPriceModal } from './advanced-gas-price-modal'; +const mockPersistGasFeePreference = jest.fn(); + jest.mock('../../gas-price-input/gas-price-input', () => ({ GasPriceInput: () =>
Gas Price Input
, })); @@ -17,10 +22,21 @@ jest.mock('../../gas-input/gas-input', () => ({ GasInput: () =>
Gas Input
, })); +jest.mock('../../../hooks/gas/usePersistGasFeePreference', () => ({ + usePersistGasFeePreference: () => mockPersistGasFeePreference, +})); + +jest.mock('../../../../../store/actions/update-transaction-gas-fees', () => ({ + updateTransactionGasFees: jest.fn(() => ({ type: 'update-gas-fees' })), +})); + const render = () => { const contractInteraction = genUnapprovedContractInteractionConfirmation({ chainId: CHAIN_IDS.GOERLI, - }); + }) as TransactionMeta; + contractInteraction.txParams.from = + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc'; + contractInteraction.txParams.gasPrice = '0x2540be400'; const store = configureStore( getMockConfirmStateForTransaction(contractInteraction), @@ -39,6 +55,7 @@ const render = () => { return { ...result, + contractInteraction, mockSetActiveModal, mockHandleCloseModals, }; @@ -80,4 +97,21 @@ describe('AdvancedGasPriceModal', () => { GasModalType.EstimatesModal, ); }); + + it('persists custom gas price preferences when Save is clicked', async () => { + const { contractInteraction, getByText, mockHandleCloseModals } = render(); + + fireEvent.click(getByText(messages.save.message)); + + await waitFor(() => { + expect(mockPersistGasFeePreference).toHaveBeenCalledWith( + contractInteraction, + { + userFeeLevel: 'custom', + gasPrice: '10', + }, + ); + }); + expect(mockHandleCloseModals).toHaveBeenCalledTimes(1); + }); }); diff --git a/ui/pages/confirmations/components/modals/advanced-gas-price-modal/advanced-gas-price-modal.tsx b/ui/pages/confirmations/components/modals/advanced-gas-price-modal/advanced-gas-price-modal.tsx index 7f8a283c84c4..844c2fd97bbd 100644 --- a/ui/pages/confirmations/components/modals/advanced-gas-price-modal/advanced-gas-price-modal.tsx +++ b/ui/pages/confirmations/components/modals/advanced-gas-price-modal/advanced-gas-price-modal.tsx @@ -30,6 +30,8 @@ import { GasInput } from '../../gas-input/gas-input'; import { useConfirmContext } from '../../../context/confirm'; import { useI18nContext } from '../../../../../hooks/useI18nContext'; import { updateTransactionGasFees } from '../../../../../store/actions/update-transaction-gas-fees'; +import { hexWEIToDecGWEI } from '../../../../../../shared/lib/conversion.utils'; +import { usePersistGasFeePreference } from '../../../hooks/gas/usePersistGasFeePreference'; export const AdvancedGasPriceModal = ({ setActiveModal, @@ -40,6 +42,7 @@ export const AdvancedGasPriceModal = ({ }) => { const t = useI18nContext(); const dispatch = useDispatch(); + const persistGasFeePreference = usePersistGasFeePreference(); const { currentConfirmation: transactionMeta } = useConfirmContext(); @@ -60,18 +63,28 @@ export const AdvancedGasPriceModal = ({ }); const hasError = Boolean(errors.gas || errors.gasPrice); - const handleSaveClick = useCallback(() => { + const handleSaveClick = useCallback(async () => { if (!transactionMeta?.id) { return; } - dispatch( + await dispatch( updateTransactionGasFees(transactionMeta.id, { userFeeLevel: UserFeeLevel.CUSTOM, ...pickBy(gasParams, Boolean), }), ); + await persistGasFeePreference(transactionMeta, { + userFeeLevel: UserFeeLevel.CUSTOM, + gasPrice: hexWEIToDecGWEI(gasParams.gasPrice), + }); handleCloseModals(); - }, [transactionMeta?.id, gasParams, handleCloseModals, dispatch]); + }, [ + transactionMeta, + gasParams, + handleCloseModals, + dispatch, + persistGasFeePreference, + ]); const navigateToEstimatesModal = useCallback(() => { setActiveModal(GasModalType.EstimatesModal); diff --git a/ui/pages/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.test.ts b/ui/pages/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.test.ts index b3ff9fa3a74f..c576824854fc 100644 --- a/ui/pages/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.test.ts +++ b/ui/pages/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react-hooks'; import { GasFeeEstimateType, GasFeeEstimateLevel, @@ -10,6 +10,8 @@ import { useFeeCalculations } from '../../components/confirm/info/hooks/useFeeCa import { useTransactionGasLimit } from './useTransactionGasLimit'; import { useGasFeeEstimateLevelOptions } from './useGasFeeEstimateLevelOptions'; +const mockPersistGasFeePreference = jest.fn(); + jest.mock('../../../../hooks/useI18nContext', () => ({ useI18nContext: () => (key: string) => key, })); @@ -30,6 +32,10 @@ jest.mock('./useTransactionGasLimit', () => ({ useTransactionGasLimit: jest.fn(), })); +jest.mock('./usePersistGasFeePreference', () => ({ + usePersistGasFeePreference: () => mockPersistGasFeePreference, +})); + jest.mock('../../../../store/actions/update-transaction-gas-fees', () => ({ updateTransactionGasFees: jest.fn(), })); @@ -173,6 +179,69 @@ describe('useGasFeeEstimateLevelOptions', () => { expect(result.current[0].value).toBe('0.001 ETH'); }); + it('persists the selected gas fee estimate level', async () => { + const transactionMeta = { + id: '1', + chainId: '0x1', + networkClientId: 'mainnet', + userFeeLevel: 'medium', + gasLimitNoBuffer: '0x5208', + txParams: { + from: '0xabc', + }, + gasFeeEstimates: { + type: GasFeeEstimateType.FeeMarket, + [GasFeeEstimateLevel.Low]: { + maxFeePerGas: '0x1', + maxPriorityFeePerGas: '0x1', + }, + [GasFeeEstimateLevel.Medium]: { + maxFeePerGas: '0x2', + maxPriorityFeePerGas: '0x2', + }, + [GasFeeEstimateLevel.High]: { + maxFeePerGas: '0x3', + maxPriorityFeePerGas: '0x3', + }, + }, + }; + + mockUseConfirmContext.mockReturnValue({ + currentConfirmation: transactionMeta, + } as unknown as ReturnType); + + mockUseGasFeeEstimates.mockReturnValue({ + gasFeeEstimates: { + [GasFeeEstimateLevel.Low]: { + minWaitTimeEstimate: 15000, + maxWaitTimeEstimate: 30000, + }, + [GasFeeEstimateLevel.Medium]: { + minWaitTimeEstimate: 10000, + maxWaitTimeEstimate: 20000, + }, + [GasFeeEstimateLevel.High]: { + minWaitTimeEstimate: 5000, + maxWaitTimeEstimate: 10000, + }, + }, + } as unknown as ReturnType); + + const { result } = renderHook(() => + useGasFeeEstimateLevelOptions({ + handleCloseModals: mockHandleCloseModals, + }), + ); + + await act(async () => { + await result.current[2].onSelect(); + }); + + expect(mockPersistGasFeePreference).toHaveBeenCalledWith(transactionMeta, { + userFeeLevel: GasFeeEstimateLevel.High, + }); + }); + it('skips high option when it has the same fees as medium for FeeMarket type', () => { mockUseConfirmContext.mockReturnValue({ currentConfirmation: { diff --git a/ui/pages/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.ts b/ui/pages/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.ts index ad549535c448..2a4ab0982016 100644 --- a/ui/pages/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.ts +++ b/ui/pages/confirmations/hooks/gas/useGasFeeEstimateLevelOptions.ts @@ -19,6 +19,7 @@ import { toHumanEstimatedTimeRange } from '../../utils/time'; import { hexWEIToDecGWEI } from '../../../../../shared/lib/conversion.utils'; import { CURRENCY_SYMBOLS } from '../../../../../shared/constants/network'; import { getNetworkConfigurationsByChainId } from '../../../../../shared/lib/selectors/networks'; +import { usePersistGasFeePreference } from './usePersistGasFeePreference'; import { useTransactionGasLimit } from './useTransactionGasLimit'; const HEX_ZERO = '0x0'; @@ -41,6 +42,7 @@ export const useGasFeeEstimateLevelOptions = ({ }): GasOption[] => { const t = useI18nContext(); const dispatch = useDispatch(); + const persistGasFeePreference = usePersistGasFeePreference(); const { currentConfirmation: transactionMeta } = useConfirmContext(); const effectiveTransactionMeta = transactionMeta ?? DUMMY_TRANSACTION_META; @@ -83,9 +85,12 @@ export const useGasFeeEstimateLevelOptions = ({ userFeeLevel: level, }), ); + await persistGasFeePreference(transactionMeta, { + userFeeLevel: level, + }); handleCloseModals(); }, - [id, handleCloseModals, dispatch], + [id, handleCloseModals, dispatch, persistGasFeePreference, transactionMeta], ); if (!transactionMeta) { diff --git a/ui/pages/confirmations/hooks/gas/useGasPriceEstimateOption.test.ts b/ui/pages/confirmations/hooks/gas/useGasPriceEstimateOption.test.ts index e344bddc3d27..ec9ec51f844b 100644 --- a/ui/pages/confirmations/hooks/gas/useGasPriceEstimateOption.test.ts +++ b/ui/pages/confirmations/hooks/gas/useGasPriceEstimateOption.test.ts @@ -1,4 +1,4 @@ -import { renderHook } from '@testing-library/react-hooks'; +import { act, renderHook } from '@testing-library/react-hooks'; import { GasFeeEstimateType, TransactionEnvelopeType, @@ -10,6 +10,7 @@ import { useFeeCalculations } from '../../components/confirm/info/hooks/useFeeCa import { useGasPriceEstimateOption } from './useGasPriceEstimateOption'; const MOCK_GAS_PRICE = '0x2540be400'; +const mockPersistGasFeePreference = jest.fn(); jest.mock('../../../../hooks/useI18nContext', () => ({ useI18nContext: () => (key: string) => key, @@ -31,6 +32,10 @@ jest.mock('../transactions/useTransactionNativeTicker', () => ({ useTransactionNativeTicker: () => 'ETH', })); +jest.mock('./usePersistGasFeePreference', () => ({ + usePersistGasFeePreference: () => mockPersistGasFeePreference, +})); + jest.mock('../../../../store/actions/update-transaction-gas-fees', () => ({ updateTransactionGasFees: jest.fn(), })); @@ -113,4 +118,42 @@ describe('useGasPriceEstimateOption', () => { expect(result.current[0].name).toBe('networkSuggested'); expect(result.current[0].isSelected).toBe(true); }); + + it('persists medium when the gas price estimate option is selected', async () => { + const transactionMeta = { + id: '1', + chainId: '0x1', + networkClientId: 'mainnet', + userFeeLevel: 'custom', + gasLimitNoBuffer: '0x5208', + gasFeeEstimates: { + type: GasFeeEstimateType.GasPrice, + gasPrice: MOCK_GAS_PRICE, + }, + txParams: { + from: '0xabc', + type: TransactionEnvelopeType.legacy, + }, + }; + + mockUseConfirmContext.mockReturnValue({ + currentConfirmation: transactionMeta, + } as unknown as ReturnType); + + mockUseGasFeeEstimates.mockReturnValue({ + gasFeeEstimates: { gasPrice: '10' }, + } as unknown as ReturnType); + + const { result } = renderHook(() => + useGasPriceEstimateOption({ handleCloseModals: mockHandleCloseModals }), + ); + + await act(async () => { + await result.current[0].onSelect(); + }); + + expect(mockPersistGasFeePreference).toHaveBeenCalledWith(transactionMeta, { + userFeeLevel: 'medium', + }); + }); }); diff --git a/ui/pages/confirmations/hooks/gas/useGasPriceEstimateOption.ts b/ui/pages/confirmations/hooks/gas/useGasPriceEstimateOption.ts index d91524e8fc45..1e1c77ea7026 100644 --- a/ui/pages/confirmations/hooks/gas/useGasPriceEstimateOption.ts +++ b/ui/pages/confirmations/hooks/gas/useGasPriceEstimateOption.ts @@ -18,6 +18,7 @@ import { EMPTY_VALUE_STRING } from '../../constants/gas'; import { useTransactionNativeTicker } from '../transactions/useTransactionNativeTicker'; import { hexWEIToDecGWEI } from '../../../../../shared/lib/conversion.utils'; import { useTransactionGasLimit } from './useTransactionGasLimit'; +import { usePersistGasFeePreference } from './usePersistGasFeePreference'; const HEX_ZERO = '0x0'; @@ -27,6 +28,7 @@ export const useGasPriceEstimateOption = ({ handleCloseModals: () => void; }): GasOption[] => { const dispatch = useDispatch(); + const persistGasFeePreference = usePersistGasFeePreference(); const t = useI18nContext(); const { currentConfirmation: transactionMeta } = useConfirmContext(); @@ -83,6 +85,9 @@ export const useGasPriceEstimateOption = ({ ...gasPropertiesToUpdate, }), ); + await persistGasFeePreference(transactionMeta, { + userFeeLevel: 'medium', + }); handleCloseModals(); }, [ id, @@ -90,6 +95,8 @@ export const useGasPriceEstimateOption = ({ transactionEnvelopeType, handleCloseModals, dispatch, + persistGasFeePreference, + transactionMeta, ]); const options = useMemo((): GasOption[] => { diff --git a/ui/pages/confirmations/hooks/gas/usePersistGasFeePreference.ts b/ui/pages/confirmations/hooks/gas/usePersistGasFeePreference.ts new file mode 100644 index 000000000000..d8231202814d --- /dev/null +++ b/ui/pages/confirmations/hooks/gas/usePersistGasFeePreference.ts @@ -0,0 +1,33 @@ +import { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { type TransactionMeta } from '@metamask/transaction-controller'; +import { type Hex } from '@metamask/utils'; +import { type AdvancedGasFeePreferences } from '../../../../../shared/constants/gas'; +import { setAdvancedGasFee } from '../../../../store/actions'; + +export function usePersistGasFeePreference() { + const dispatch = useDispatch(); + + return useCallback( + async ( + transactionMeta: TransactionMeta | undefined, + gasFeePreferences: AdvancedGasFeePreferences, + ) => { + const account = transactionMeta?.txParams?.from as Hex | undefined; + const chainId = transactionMeta?.chainId; + + if (!account || !chainId) { + return; + } + + await dispatch( + setAdvancedGasFee({ + account, + chainId, + gasFeePreferences, + }), + ); + }, + [dispatch], + ); +} diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 99b4a577f08b..5878029ee462 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2759,26 +2759,17 @@ export const getNetworkClientIdsToPoll = createDeepEqualSelector( * To retrieve the maxBaseFee and priorityFee the user has set as default * * @param {*} state - * @returns {{maxBaseFee: string, priorityFee: string} | undefined} + * @returns {{userFeeLevel: string, maxBaseFee?: string, priorityFee?: string, gasPrice?: string} | undefined} */ export function getAdvancedGasFeeValues(state) { - // This will not work when we switch to supporting multi-chain. - // There are four non-test files that use this selector. - // advanced-gas-fee-defaults - // base-fee-input - // priority-fee-input - // useGasItemFeeDetails - // The first three are part of the AdvancedGasFeePopover - // The latter is used by the EditGasPopover - // Both of those are used in Confirmations as well as transaction-list-item - // All of the call sites have access to the GasFeeContext, which has a - // transaction object set on it, but there are currently no guarantees that - // the transaction has a chainId associated with it. To have this method - // support multichain we'll need a reliable way for the chainId of the - // transaction being modified to be available to all callsites and either - // pass it in to the selector as a second parameter, or access it at the - // callsite. - return state.metamask.advancedGasFee[getCurrentChainId(state)]; + const selectedAccount = getSelectedInternalAccount(state); + const account = selectedAccount?.address?.toLowerCase(); + + if (!account) { + return undefined; + } + + return state.metamask.advancedGasFee[getCurrentChainId(state)]?.[account]; } /** diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index 6309c17cdcb2..7453c6f1c9c3 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -1122,6 +1122,7 @@ describe('Selectors', () => { it('#getAdvancedGasFeeValues', () => { const advancedGasFee = selectors.getAdvancedGasFeeValues(mockState); expect(advancedGasFee).toStrictEqual({ + userFeeLevel: 'custom', maxBaseFee: '75', priorityFee: '2', }); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index e4e1c54174cd..373cd888fe2f 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -169,7 +169,10 @@ import { loadRelativeTimeFormatLocaleData, } from '../../shared/lib/i18n'; import { decimalToHex } from '../../shared/lib/conversion.utils'; -import { PriorityLevels } from '../../shared/constants/gas'; +import { + type AdvancedGasFeePreferences, + PriorityLevels, +} from '../../shared/constants/gas'; import { getErrorMessage, isErrorWithMessage, @@ -4983,9 +4986,11 @@ export function detectNfts( }; } -export function setAdvancedGasFee( - val: { chainId: Hex; maxBaseFee?: string; priorityFee?: string } | null, -): ThunkAction { +export function setAdvancedGasFee(val: { + account: Hex; + chainId: Hex; + gasFeePreferences?: AdvancedGasFeePreferences; +}): ThunkAction { return async (dispatch: MetaMaskReduxDispatch) => { dispatch(showLoadingIndication()); log.debug(`background.setAdvancedGasFee`); diff --git a/yarn.lock b/yarn.lock index dd0b1aa32812..b30bde55c5e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5684,15 +5684,15 @@ __metadata: languageName: node linkType: hard -"@metamask/accounts-controller@npm:^39.0.0": - version: 39.0.0 - resolution: "@metamask/accounts-controller@npm:39.0.0" +"@metamask/accounts-controller@npm:^39.0.0, @metamask/accounts-controller@npm:^39.0.1": + version: 39.0.1 + resolution: "@metamask/accounts-controller@npm:39.0.1" dependencies: "@ethereumjs/util": "npm:^9.1.0" "@metamask/base-controller": "npm:^9.1.0" "@metamask/eth-snap-keyring": "npm:^22.0.1" "@metamask/keyring-api": "npm:^23.1.0" - "@metamask/keyring-controller": "npm:^26.0.0" + "@metamask/keyring-controller": "npm:^27.0.0" "@metamask/keyring-internal-api": "npm:^11.0.1" "@metamask/keyring-sdk": "npm:^2.1.1" "@metamask/keyring-utils": "npm:^3.2.1" @@ -5708,7 +5708,7 @@ __metadata: peerDependencies: "@metamask/providers": ^22.0.0 webextension-polyfill: ^0.10.0 || ^0.11.0 || ^0.12.0 - checksum: 10/1cdaae1818af96e1cccfb95452a54a936299f6aadc1f8f39a5651dbe409eced0379bcd959c2e944caad3fbba91a3b7c7e4a954a8a1eb9804302bfa0336ad7ac6 + checksum: 10/2ca9f25e55830676cb69f55d6b31601652590ff86194fa6f98fdb1c2cc32dce0c735c3de1cda9081ae6359e2d10b52034952519e783e37e4485e0921c2a7182d languageName: node linkType: hard @@ -5774,16 +5774,16 @@ __metadata: languageName: node linkType: hard -"@metamask/approval-controller@npm:^9.0.0, @metamask/approval-controller@npm:^9.0.1": - version: 9.0.1 - resolution: "@metamask/approval-controller@npm:9.0.1" +"@metamask/approval-controller@npm:^9.0.0, @metamask/approval-controller@npm:^9.0.1, @metamask/approval-controller@npm:^9.0.2": + version: 9.0.2 + resolution: "@metamask/approval-controller@npm:9.0.2" dependencies: - "@metamask/base-controller": "npm:^9.0.1" - "@metamask/messenger": "npm:^1.0.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/messenger": "npm:^1.2.0" "@metamask/rpc-errors": "npm:^7.0.2" "@metamask/utils": "npm:^11.9.0" nanoid: "npm:^3.3.8" - checksum: 10/980e7ded7022a887c11693226922f9814d160c93fe5297380addafebe9b6e9191ba3acc7bf54775c8c8eeb7e07bcfcaaf79cc90361ff18fa04c1d449eab2ed33 + checksum: 10/f27d75901a1a8367a8972b30ebb097a063282c8eb40f3188a6255ed1e043313085de6d5feb9300660366d4d93c822ebe6b37219978806db0492c4fed761d3c6a languageName: node linkType: hard @@ -6211,10 +6211,11 @@ __metadata: languageName: node linkType: hard -"@metamask/controller-utils@npm:^12.0.0, @metamask/controller-utils@npm:^12.1.0": - version: 12.1.0 - resolution: "@metamask/controller-utils@npm:12.1.0" +"@metamask/controller-utils@npm:^12.0.0, @metamask/controller-utils@npm:^12.1.0, @metamask/controller-utils@npm:^12.1.1, @metamask/controller-utils@npm:^12.2.0": + version: 12.2.0 + resolution: "@metamask/controller-utils@npm:12.2.0" dependencies: + "@ethersproject/abi": "npm:^5.7.0" "@metamask/eth-query": "npm:^4.0.0" "@metamask/ethjs-unit": "npm:^0.3.0" "@metamask/utils": "npm:^11.9.0" @@ -6228,24 +6229,24 @@ __metadata: lodash: "npm:^4.17.21" peerDependencies: "@babel/runtime": ^7.0.0 - checksum: 10/421b9685faefa481529e611a11cf005cd94ced8f4a15a6ea1aacd885417be0de6191ee414e0bcb0fff7ecade9e356a4d0f3d7c826e798ba47d65e412aa1df049 + checksum: 10/24551cec486319b39e0db6792e5d26bafa7fb87dce63d516cc80a5645f7c185b7911e89d21189e7270d338436074bc6e1e6e0983b008cb130292ee1e1902c28d languageName: node linkType: hard -"@metamask/core-backend@npm:^6.1.1, @metamask/core-backend@npm:^6.2.1, @metamask/core-backend@npm:^6.2.2, @metamask/core-backend@npm:^6.3.0, @metamask/core-backend@npm:^6.3.2": - version: 6.3.2 - resolution: "@metamask/core-backend@npm:6.3.2" +"@metamask/core-backend@npm:^6.1.1, @metamask/core-backend@npm:^6.2.1, @metamask/core-backend@npm:^6.2.2, @metamask/core-backend@npm:^6.3.0, @metamask/core-backend@npm:^6.3.2, @metamask/core-backend@npm:^6.3.3": + version: 6.3.3 + resolution: "@metamask/core-backend@npm:6.3.3" dependencies: - "@metamask/accounts-controller": "npm:^39.0.0" - "@metamask/controller-utils": "npm:^12.1.0" - "@metamask/keyring-controller": "npm:^26.0.0" + "@metamask/accounts-controller": "npm:^39.0.1" + "@metamask/controller-utils": "npm:^12.1.1" + "@metamask/keyring-controller": "npm:^27.0.0" "@metamask/messenger": "npm:^1.2.0" "@metamask/profile-sync-controller": "npm:^28.1.1" "@metamask/utils": "npm:^11.9.0" "@tanstack/query-core": "npm:^5.62.16" async-mutex: "npm:^0.5.0" uuid: "npm:^8.3.2" - checksum: 10/97965ba67fc3574b857a8f18b66450e141d3ee58afe0a2788d08e55253183692421373c0c076ae75e5763f5206eb7be1af4235d4ab480881a9b80fcec6538541 + checksum: 10/be94b9131e1babea048a60f058c345d42ea8423fc3a7121013be4713f7d6298a2b9e3d2c454d0a8ddfd21216f3520bf517556b9d7b5b16600f700bf7cb5d98b6 languageName: node linkType: hard @@ -7111,6 +7112,29 @@ __metadata: languageName: node linkType: hard +"@metamask/keyring-controller@npm:^27.0.0": + version: 27.0.0 + resolution: "@metamask/keyring-controller@npm:27.0.0" + dependencies: + "@ethereumjs/util": "npm:^9.1.0" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/browser-passworder": "npm:^6.0.0" + "@metamask/eth-hd-keyring": "npm:^14.1.1" + "@metamask/eth-sig-util": "npm:^8.2.0" + "@metamask/eth-simple-keyring": "npm:^12.0.2" + "@metamask/keyring-api": "npm:^23.1.0" + "@metamask/keyring-internal-api": "npm:^11.0.1" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/utils": "npm:^11.9.0" + async-mutex: "npm:^0.5.0" + ethereumjs-wallet: "npm:^1.0.1" + immer: "npm:^9.0.6" + lodash: "npm:^4.17.21" + ulid: "npm:^2.3.0" + checksum: 10/42e21a4f747bd40db72587065cf5f60cf916b3e70f2bb6e368cd3c31220f05b44673a90302e8dd98212dcf098aca5a7c553dcf3fd21e0f570190b768963e6e7e + languageName: node + linkType: hard + "@metamask/keyring-internal-api@npm:^11.0.1": version: 11.0.1 resolution: "@metamask/keyring-internal-api@npm:11.0.1" @@ -8457,6 +8481,44 @@ __metadata: languageName: node linkType: hard +"@metamask/transaction-controller@npm:@metamask-previews/transaction-controller@68.0.0-preview-1a8241c": + version: 68.0.0-preview-1a8241c + resolution: "@metamask-previews/transaction-controller@npm:68.0.0-preview-1a8241c" + dependencies: + "@ethereumjs/common": "npm:^4.4.0" + "@ethereumjs/tx": "npm:^5.4.0" + "@ethereumjs/util": "npm:^9.1.0" + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/contracts": "npm:^5.7.0" + "@ethersproject/providers": "npm:^5.7.0" + "@ethersproject/wallet": "npm:^5.7.0" + "@metamask/accounts-controller": "npm:^39.0.1" + "@metamask/approval-controller": "npm:^9.0.2" + "@metamask/base-controller": "npm:^9.1.0" + "@metamask/controller-utils": "npm:^12.2.0" + "@metamask/core-backend": "npm:^6.3.3" + "@metamask/gas-fee-controller": "npm:^26.2.2" + "@metamask/messenger": "npm:^1.2.0" + "@metamask/metamask-eth-abis": "npm:^3.1.1" + "@metamask/network-controller": "npm:^32.0.0" + "@metamask/nonce-tracker": "npm:^6.0.0" + "@metamask/remote-feature-flag-controller": "npm:^4.2.2" + "@metamask/rpc-errors": "npm:^7.0.2" + "@metamask/utils": "npm:^11.11.0" + async-mutex: "npm:^0.5.0" + bignumber.js: "npm:^9.1.2" + bn.js: "npm:^5.2.1" + eth-method-registry: "npm:^4.0.0" + fast-json-patch: "npm:^3.1.1" + lodash: "npm:^4.17.21" + uuid: "npm:^8.3.2" + peerDependencies: + "@babel/runtime": ^7.0.0 + "@metamask/eth-block-tracker": ">=9" + checksum: 10/4b44f8c1faf6e7cbb655b0d05133bfa1884db98449c6df12c8bbc0aac9cc94bc9a115a0a4b051d109c411aeef49daaff17ca436ad9e128019e428819b0ff4e81 + languageName: node + linkType: hard + "@metamask/transaction-controller@npm:^62.12.0": version: 62.22.0 resolution: "@metamask/transaction-controller@npm:62.22.0" @@ -8610,44 +8672,6 @@ __metadata: languageName: node linkType: hard -"@metamask/transaction-controller@npm:^67.0.0": - version: 67.0.0 - resolution: "@metamask/transaction-controller@npm:67.0.0" - dependencies: - "@ethereumjs/common": "npm:^4.4.0" - "@ethereumjs/tx": "npm:^5.4.0" - "@ethereumjs/util": "npm:^9.1.0" - "@ethersproject/abi": "npm:^5.7.0" - "@ethersproject/contracts": "npm:^5.7.0" - "@ethersproject/providers": "npm:^5.7.0" - "@ethersproject/wallet": "npm:^5.7.0" - "@metamask/accounts-controller": "npm:^39.0.0" - "@metamask/approval-controller": "npm:^9.0.1" - "@metamask/base-controller": "npm:^9.1.0" - "@metamask/controller-utils": "npm:^12.1.0" - "@metamask/core-backend": "npm:^6.3.2" - "@metamask/gas-fee-controller": "npm:^26.2.2" - "@metamask/messenger": "npm:^1.2.0" - "@metamask/metamask-eth-abis": "npm:^3.1.1" - "@metamask/network-controller": "npm:^32.0.0" - "@metamask/nonce-tracker": "npm:^6.0.0" - "@metamask/remote-feature-flag-controller": "npm:^4.2.2" - "@metamask/rpc-errors": "npm:^7.0.2" - "@metamask/utils": "npm:^11.9.0" - async-mutex: "npm:^0.5.0" - bignumber.js: "npm:^9.1.2" - bn.js: "npm:^5.2.1" - eth-method-registry: "npm:^4.0.0" - fast-json-patch: "npm:^3.1.1" - lodash: "npm:^4.17.21" - uuid: "npm:^8.3.2" - peerDependencies: - "@babel/runtime": ^7.0.0 - "@metamask/eth-block-tracker": ">=9" - checksum: 10/600ff572c5b69b5a394d73cdf7097c299a8877f396776c4f6ecdc429562e886ee592ef2ca1c03ac215cffa8ddce03cb0a527b0be9c59493452153c3384ceacd5 - languageName: node - linkType: hard - "@metamask/transaction-pay-controller@npm:22.6.0": version: 22.6.0 resolution: "@metamask/transaction-pay-controller@npm:22.6.0"