From 0c525203f2a56480b92e36893922c87c63ed53ec Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Wed, 15 Jul 2026 20:07:47 +0200 Subject: [PATCH 1/2] feat(wallet)!: wire `GasFeeController` into default initialization Wire `GasFeeController` into the wallet's default set as its own `InitializationConfiguration`, with all client-varying values injectable via `instanceOptions.gasFeeController` and platform-agnostic defaults. This resolves the lazy `GasFeeController:fetchGasFeeEstimates` delegation already registered by the wired `TransactionController`. Because `GasFeeController`'s constructor eagerly reads `NetworkController` state, add an optional `dependencies` field to `InitializationConfiguration` and a stable topological sort in `initialize` so dependencies are always constructed first (the default set was otherwise ordered alphabetically). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/CODEOWNERS | 1 + README.md | 1 + packages/wallet/CHANGELOG.md | 4 + packages/wallet/package.json | 1 + .../src/initialization/initialization.test.ts | 74 ++++++ .../src/initialization/initialization.ts | 51 +++- .../gas-fee-controller.test.ts | 236 ++++++++++++++++++ .../gas-fee-controller/gas-fee-controller.ts | 81 ++++++ .../instances/gas-fee-controller/types.ts | 26 ++ .../src/initialization/instances/index.ts | 1 + packages/wallet/src/initialization/types.ts | 6 + packages/wallet/src/types.ts | 2 + packages/wallet/tsconfig.build.json | 1 + packages/wallet/tsconfig.json | 3 + yarn.lock | 1 + 15 files changed, 485 insertions(+), 4 deletions(-) create mode 100644 packages/wallet/src/initialization/initialization.test.ts create mode 100644 packages/wallet/src/initialization/instances/gas-fee-controller/gas-fee-controller.test.ts create mode 100644 packages/wallet/src/initialization/instances/gas-fee-controller/gas-fee-controller.ts create mode 100644 packages/wallet/src/initialization/instances/gas-fee-controller/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 49f39f1ce0..d6b38990b9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -146,6 +146,7 @@ /packages/wallet/src/initialization/instances/address-book-controller/ @MetaMask/confirmations /packages/wallet/src/initialization/instances/approval-controller/ @MetaMask/confirmations /packages/wallet/src/initialization/instances/connectivity-controller/ @MetaMask/core-platform +/packages/wallet/src/initialization/instances/gas-fee-controller/ @MetaMask/confirmations /packages/wallet/src/initialization/instances/keyring-controller/ @MetaMask/accounts-engineers @MetaMask/core-platform /packages/wallet/src/initialization/instances/remote-feature-flag-controller/ @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform /packages/wallet/src/initialization/instances/storage-service/ @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform diff --git a/README.md b/README.md index fc19b98258..2abbf0b897 100644 --- a/README.md +++ b/README.md @@ -645,6 +645,7 @@ linkStyle default opacity:0.5 wallet --> base_controller; wallet --> connectivity_controller; wallet --> controller_utils; + wallet --> gas_fee_controller; wallet --> keyring_controller; wallet --> messenger; wallet --> network_controller; diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 706c4ac482..d9412ef0a0 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **BREAKING:** Wire `GasFeeController` into the default wallet initialization ([#9510](https://github.com/MetaMask/core/pull/9510)) + ## [7.0.1] ### Changed diff --git a/packages/wallet/package.json b/packages/wallet/package.json index a31271b98a..4b4710e13b 100644 --- a/packages/wallet/package.json +++ b/packages/wallet/package.json @@ -62,6 +62,7 @@ "@metamask/browser-passworder": "^6.0.0", "@metamask/connectivity-controller": "^0.3.0", "@metamask/controller-utils": "^12.3.0", + "@metamask/gas-fee-controller": "^26.2.4", "@metamask/keyring-controller": "^27.1.0", "@metamask/messenger": "^2.0.0", "@metamask/network-controller": "^34.0.0", diff --git a/packages/wallet/src/initialization/initialization.test.ts b/packages/wallet/src/initialization/initialization.test.ts new file mode 100644 index 0000000000..fba0a0456e --- /dev/null +++ b/packages/wallet/src/initialization/initialization.test.ts @@ -0,0 +1,74 @@ +import { orderByDependencies } from './initialization'; +import type { InitializationConfiguration } from './types'; + +/** + * Builds a minimal initialization configuration for ordering tests. Only `name` + * and `dependencies` are relevant here. + * + * @param name - The instance name. + * @param dependencies - Names of instances that must be constructed first. + * @returns A stub initialization configuration. + */ +function config( + name: string, + dependencies?: string[], +): InitializationConfiguration { + return { + name, + dependencies, + init: jest.fn(), + getMessenger: jest.fn(), + } as unknown as InitializationConfiguration; +} + +/** + * Extracts the names of ordered configurations. + * + * @param configurations - The configurations to name. + * @returns The names in order. + */ +function names( + configurations: InitializationConfiguration[], +): string[] { + return configurations.map((configuration) => configuration.name); +} + +describe('orderByDependencies', () => { + it('preserves the order of configurations without dependencies', () => { + const configurations = [config('A'), config('B'), config('C')]; + + expect(names(orderByDependencies(configurations))).toStrictEqual([ + 'A', + 'B', + 'C', + ]); + }); + + it('constructs a dependency before the configuration that depends on it', () => { + const configurations = [config('A', ['B']), config('B'), config('C')]; + + expect(names(orderByDependencies(configurations))).toStrictEqual([ + 'B', + 'A', + 'C', + ]); + }); + + it('treats dependencies outside the set as already satisfied', () => { + const configurations = [config('A', ['External']), config('B')]; + + expect(names(orderByDependencies(configurations))).toStrictEqual([ + 'A', + 'B', + ]); + }); + + it('falls back to the remaining order when a dependency cycle exists', () => { + const configurations = [config('A', ['B']), config('B', ['A'])]; + + expect(names(orderByDependencies(configurations))).toStrictEqual([ + 'A', + 'B', + ]); + }); +}); diff --git a/packages/wallet/src/initialization/initialization.ts b/packages/wallet/src/initialization/initialization.ts index 28a7b835f5..16c7038203 100644 --- a/packages/wallet/src/initialization/initialization.ts +++ b/packages/wallet/src/initialization/initialization.ts @@ -29,10 +29,12 @@ export function initialize(options: InitializeOptions): DefaultInstances { (config) => config.name, ); - const configurationEntries = initializationConfigurations.concat( - Object.values(defaultConfigurations).filter( - (config) => !overriddenConfiguration.includes(config.name), - ) as InitializationConfiguration[], + const configurationEntries = orderByDependencies( + initializationConfigurations.concat( + Object.values(defaultConfigurations).filter( + (config) => !overriddenConfiguration.includes(config.name), + ) as InitializationConfiguration[], + ), ); const instances: Record = {}; @@ -59,3 +61,44 @@ export function initialize(options: InitializeOptions): DefaultInstances { return instances as DefaultInstances; } + +/** + * Orders initialization configurations so each one is constructed after its + * declared dependencies. The relative order of configurations without an unmet + * dependency is preserved, and dependencies outside the set are treated as + * already satisfied. + * + * @param configurations - The configurations to order. + * @returns The configurations ordered so that dependencies come first. + */ +export function orderByDependencies( + configurations: InitializationConfiguration[], +): InitializationConfiguration[] { + const availableNames = new Set(configurations.map((config) => config.name)); + const remaining = [...configurations]; + const initialized = new Set(); + const ordered: InitializationConfiguration[] = []; + + while (remaining.length > 0) { + const index = remaining.findIndex((config) => + (config.dependencies ?? []).every( + (dependency) => + !availableNames.has(dependency) || initialized.has(dependency), + ), + ); + + if (index === -1) { + // A dependency cycle remains. Emit the rest in their current order rather + // than looping forever; the messenger surfaces a clear "handler has not + // been registered" error at construction time. + ordered.push(...remaining); + break; + } + + const [config] = remaining.splice(index, 1); + ordered.push(config); + initialized.add(config.name); + } + + return ordered; +} diff --git a/packages/wallet/src/initialization/instances/gas-fee-controller/gas-fee-controller.test.ts b/packages/wallet/src/initialization/instances/gas-fee-controller/gas-fee-controller.test.ts new file mode 100644 index 0000000000..5344adcbb3 --- /dev/null +++ b/packages/wallet/src/initialization/instances/gas-fee-controller/gas-fee-controller.test.ts @@ -0,0 +1,236 @@ +import { GasFeeController } from '@metamask/gas-fee-controller'; +import { Messenger } from '@metamask/messenger'; +import { InMemoryStorageAdapter } from '@metamask/storage-service'; + +import type { WalletOptions } from '../../../types'; +import { Wallet } from '../../../Wallet'; +import { defaultConfigurations } from '../../defaults'; +import type { + DefaultActions, + DefaultEvents, + RootMessenger, +} from '../../defaults'; +import { AlwaysOnlineAdapter } from '../connectivity-controller/always-online-adapter'; +import { gasFeeController } from './gas-fee-controller'; + +const controllers: GasFeeController[] = []; +const wallets: Wallet[] = []; + +const REMOTE_FEATURE_FLAG_OPTIONS = { + clientConfigApiService: { + fetchRemoteFeatureFlags: async (): Promise<{ + remoteFeatureFlags: Record; + cacheTimestamp: number; + }> => ({ remoteFeatureFlags: {}, cacheTimestamp: Date.now() }), + }, +}; + +type ActionHandler = (...args: unknown[]) => unknown; + +type AnyMessenger = Messenger; + +describe('gasFeeController', () => { + afterEach(async () => { + for (const controller of controllers.splice(0)) { + controller.destroy(); + } + + await Promise.all(wallets.splice(0).map((wallet) => wallet.destroy())); + }); + + it('is registered as a default initialization configuration', () => { + expect(Object.values(defaultConfigurations)).toContain(gasFeeController); + }); + + it('initializes a GasFeeController with default state', () => { + const rootMessenger = getRootMessenger(); + const messenger = gasFeeController.getMessenger(rootMessenger); + + const instance = gasFeeController.init({ + state: undefined, + messenger, + options: {}, + }); + controllers.push(instance); + + expect(instance).toBeInstanceOf(GasFeeController); + expect(rootMessenger.call('GasFeeController:getState')).toStrictEqual({ + gasFeeEstimatesByChainId: {}, + gasFeeEstimates: {}, + estimatedGasFeeTimeBounds: {}, + gasEstimateType: 'none', + nonRPCGasFeeApisDisabled: false, + }); + }); + + it('is initialized by the default Wallet configuration', () => { + const wallet = new Wallet({ + instanceOptions: getInstanceOptions(), + }); + wallets.push(wallet); + + expect(wallet.getInstance('GasFeeController')).toBeInstanceOf( + GasFeeController, + ); + }); + + it('exposes GasFeeController:fetchGasFeeEstimates over the shared bus, resolving the TransactionController delegation', async () => { + // The messenger binds the method at construction, so spy on the prototype + // before building the wallet. This also avoids the real network fetch. + const fetchGasFeeEstimates = jest + .spyOn(GasFeeController.prototype, 'fetchGasFeeEstimates') + .mockResolvedValue({ + gasFeeEstimatesByChainId: {}, + gasFeeEstimates: {}, + estimatedGasFeeTimeBounds: {}, + gasEstimateType: 'none', + nonRPCGasFeeApisDisabled: false, + }); + + const wallet = new Wallet({ + instanceOptions: getInstanceOptions(), + }); + wallets.push(wallet); + + await wallet.messenger.call('GasFeeController:fetchGasFeeEstimates'); + + expect(fetchGasFeeEstimates).toHaveBeenCalled(); + }); + + it('forwards the provided state to the controller', () => { + const rootMessenger = getRootMessenger(); + const messenger = gasFeeController.getMessenger(rootMessenger); + + const instance = gasFeeController.init({ + state: { + gasFeeEstimatesByChainId: {}, + gasFeeEstimates: {}, + estimatedGasFeeTimeBounds: {}, + gasEstimateType: 'none', + nonRPCGasFeeApisDisabled: true, + }, + messenger, + options: {}, + }); + controllers.push(instance); + + expect(instance.state.nonRPCGasFeeApisDisabled).toBe(true); + }); + + it('builds the network callbacks from the wired NetworkController', async () => { + const rootMessenger = getRootMessenger(); + const messenger = gasFeeController.getMessenger(rootMessenger); + + const instance = gasFeeController.init({ + state: undefined, + messenger, + options: {}, + }); + controllers.push(instance); + + // Drives `getProvider` and the EIP-1559/legacy/account callbacks; the + // estimate then fails at the offline provider, which is irrelevant here. + await expect(instance.fetchGasFeeEstimates()).rejects.toThrow( + 'Gas fee/price estimation failed', + ); + }); + + it('applies injectable options over the defaults', () => { + const rootMessenger = getRootMessenger(); + const messenger = gasFeeController.getMessenger(rootMessenger); + const getCurrentAccountEIP1559Compatibility = jest + .fn() + .mockReturnValue(false); + const getCurrentNetworkLegacyGasAPICompatibility = jest + .fn() + .mockReturnValue(true); + + const instance = gasFeeController.init({ + state: undefined, + messenger, + options: { + EIP1559APIEndpoint: 'https://example.test//eip1559', + legacyAPIEndpoint: 'https://example.test//legacy', + clientId: 'test-client', + interval: 30_000, + getCurrentAccountEIP1559Compatibility, + getCurrentNetworkLegacyGasAPICompatibility, + }, + }); + controllers.push(instance); + + expect(instance).toBeInstanceOf(GasFeeController); + }); +}); + +function getRootMessenger(): RootMessenger { + const rootMessenger = new Messenger<'Root', DefaultActions, DefaultEvents>({ + namespace: 'Root', + }); + + registerActionHandler( + rootMessenger, + 'NetworkController', + 'NetworkController:getState', + jest.fn().mockReturnValue({ selectedNetworkClientId: 'mainnet' }), + ); + + registerActionHandler( + rootMessenger, + 'NetworkController', + 'NetworkController:getNetworkClientById', + jest.fn().mockReturnValue({ + // Errors immediately so `eth_gasPrice` fallback settles without a network + // call rather than hanging. + provider: { + sendAsync: (_request: unknown, callback: (error: Error) => void) => + callback(new Error('offline')), + }, + configuration: { chainId: '0x1' }, + }), + ); + + registerActionHandler( + rootMessenger, + 'NetworkController', + 'NetworkController:getEIP1559Compatibility', + // `false` skips the EIP-1559/legacy API fetches, so a fetch goes straight to + // the offline provider above. + jest.fn().mockResolvedValue(false), + ); + + return rootMessenger; +} + +function getInstanceOptions(): WalletOptions['instanceOptions'] { + return { + connectivityController: { + connectivityAdapter: new AlwaysOnlineAdapter(), + }, + networkController: { + infuraProjectId: 'test-infura-project-id', + }, + storageService: { + storage: new InMemoryStorageAdapter(), + }, + remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS, + }; +} + +function registerActionHandler( + parent: RootMessenger, + namespace: string, + actionType: string, + handler: ActionHandler, +): void { + const messenger = new Messenger({ + namespace, + parent: parent as unknown as AnyMessenger, + }); + + ( + messenger as unknown as { + registerActionHandler(type: string, handler: ActionHandler): void; + } + ).registerActionHandler(actionType, handler); +} diff --git a/packages/wallet/src/initialization/instances/gas-fee-controller/gas-fee-controller.ts b/packages/wallet/src/initialization/instances/gas-fee-controller/gas-fee-controller.ts new file mode 100644 index 0000000000..6ad12b6b54 --- /dev/null +++ b/packages/wallet/src/initialization/instances/gas-fee-controller/gas-fee-controller.ts @@ -0,0 +1,81 @@ +import type { GasFeeMessenger } from '@metamask/gas-fee-controller'; +import { GasFeeController } from '@metamask/gas-fee-controller'; +import { Messenger } from '@metamask/messenger'; +import type { ProviderProxy } from '@metamask/network-controller'; + +import type { InitializationConfiguration } from '../../types'; + +export type { GasFeeControllerInstanceOptions } from './types'; + +const GAS_API_BASE_URL = 'https://gas.api.cx.metamask.io'; + +// The controller substitutes `` with the decimal chain ID per request. +const DEFAULT_EIP1559_API_ENDPOINT = `${GAS_API_BASE_URL}/networks//suggestedGasFees`; +const DEFAULT_LEGACY_API_ENDPOINT = `${GAS_API_BASE_URL}/networks//gasPrices`; +const DEFAULT_CLIENT_ID = 'cli'; + +export const gasFeeController: InitializationConfiguration< + GasFeeController, + GasFeeMessenger +> = { + name: 'GasFeeController', + // The constructor eagerly reads `NetworkController` state to build its + // provider, so `NetworkController` must be constructed first. + dependencies: ['NetworkController'], + init: ({ state, messenger, options }) => { + const { + EIP1559APIEndpoint = DEFAULT_EIP1559_API_ENDPOINT, + legacyAPIEndpoint = DEFAULT_LEGACY_API_ENDPOINT, + clientId = DEFAULT_CLIENT_ID, + interval, + getCurrentNetworkLegacyGasAPICompatibility = (): boolean => false, + getCurrentAccountEIP1559Compatibility = (): boolean => true, + } = options; + + return new GasFeeController({ + messenger, + state, + interval, + EIP1559APIEndpoint, + legacyAPIEndpoint, + clientId, + getCurrentNetworkLegacyGasAPICompatibility, + getCurrentAccountEIP1559Compatibility, + // Built from `NetworkController`, which the controller expects as direct + // callbacks rather than messenger actions. + getProvider: (): ProviderProxy => { + const { selectedNetworkClientId } = messenger.call( + 'NetworkController:getState', + ); + // The client's provider proxy lacks `setTarget`, which `ProviderProxy` + // nominally requires but `GasFeeController` never calls. + return messenger.call( + 'NetworkController:getNetworkClientById', + selectedNetworkClientId, + ).provider as ProviderProxy; + }, + getCurrentNetworkEIP1559Compatibility: async () => + Boolean( + await messenger.call('NetworkController:getEIP1559Compatibility'), + ), + }); + }, + getMessenger: (parent) => { + const messenger: GasFeeMessenger = new Messenger({ + namespace: 'GasFeeController', + parent, + }); + + parent.delegate({ + messenger, + actions: [ + 'NetworkController:getEIP1559Compatibility', + 'NetworkController:getNetworkClientById', + 'NetworkController:getState', + ], + events: ['NetworkController:networkDidChange'], + }); + + return messenger; + }, +}; diff --git a/packages/wallet/src/initialization/instances/gas-fee-controller/types.ts b/packages/wallet/src/initialization/instances/gas-fee-controller/types.ts new file mode 100644 index 0000000000..202cc64e49 --- /dev/null +++ b/packages/wallet/src/initialization/instances/gas-fee-controller/types.ts @@ -0,0 +1,26 @@ +import type { GasFeeController } from '@metamask/gas-fee-controller'; + +type GasFeeControllerOptions = ConstructorParameters< + typeof GasFeeController +>[0]; + +/** + * Per-instance options for the wallet's `GasFeeController`. Every field is + * optional; the instance's `init` applies a platform-agnostic default for each + * one, which clients (extension, mobile, wallet-cli) override. + */ +export type GasFeeControllerInstanceOptions = { + /** EIP-1559 gas price API URL. Defaults to the production endpoint. */ + // eslint-disable-next-line @typescript-eslint/naming-convention + EIP1559APIEndpoint?: GasFeeControllerOptions['EIP1559APIEndpoint']; + /** Legacy gas price API URL. Defaults to the production endpoint. */ + legacyAPIEndpoint?: GasFeeControllerOptions['legacyAPIEndpoint']; + /** Sent as `X-Client-Id` to the gas API. Defaults to `'cli'`. */ + clientId?: GasFeeControllerOptions['clientId']; + /** Milliseconds between polls. Defaults to the controller's own 15 seconds. */ + interval?: GasFeeControllerOptions['interval']; + /** Defaults to `() => false`. */ + getCurrentNetworkLegacyGasAPICompatibility?: GasFeeControllerOptions['getCurrentNetworkLegacyGasAPICompatibility']; + /** Defaults to `() => true`. */ + getCurrentAccountEIP1559Compatibility?: GasFeeControllerOptions['getCurrentAccountEIP1559Compatibility']; +}; diff --git a/packages/wallet/src/initialization/instances/index.ts b/packages/wallet/src/initialization/instances/index.ts index a15db95603..83401bec86 100644 --- a/packages/wallet/src/initialization/instances/index.ts +++ b/packages/wallet/src/initialization/instances/index.ts @@ -2,6 +2,7 @@ export { accountsController } from './accounts-controller/accounts-controller'; export { addressBookController } from './address-book-controller/address-book-controller'; export { approvalController } from './approval-controller/approval-controller'; export { connectivityController } from './connectivity-controller/connectivity-controller'; +export { gasFeeController } from './gas-fee-controller/gas-fee-controller'; export { keyringController } from './keyring-controller/keyring-controller'; export { networkController } from './network-controller/network-controller'; export { remoteFeatureFlagController } from './remote-feature-flag-controller/remote-feature-flag-controller'; diff --git a/packages/wallet/src/initialization/types.ts b/packages/wallet/src/initialization/types.ts index 1e7e51602b..649387cb5d 100644 --- a/packages/wallet/src/initialization/types.ts +++ b/packages/wallet/src/initialization/types.ts @@ -45,6 +45,12 @@ export type InitFunctionArguments = { export type InitializationConfiguration = { name: InstanceName; + /** + * Names of instances that must be constructed first, because this instance's + * constructor eagerly calls their messenger actions (e.g. `GasFeeController` + * reads `NetworkController` state). + */ + dependencies?: string[]; init(args: InitFunctionArguments): Instance; getMessenger( parent: RootMessenger, diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 1f330e9baf..896eee8d89 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -7,6 +7,7 @@ import type { } from './initialization/defaults'; import type { ApprovalControllerInstanceOptions } from './initialization/instances/approval-controller/types'; import type { ConnectivityControllerInstanceOptions } from './initialization/instances/connectivity-controller/types'; +import type { GasFeeControllerInstanceOptions } from './initialization/instances/gas-fee-controller/types'; import type { KeyringControllerInstanceOptions } from './initialization/instances/keyring-controller/types'; import type { NetworkControllerInstanceOptions } from './initialization/instances/network-controller/types'; import type { RemoteFeatureFlagControllerInstanceOptions } from './initialization/instances/remote-feature-flag-controller/types'; @@ -27,6 +28,7 @@ export type WalletOptions = { export type InstanceSpecificOptions = { approvalController?: ApprovalControllerInstanceOptions; connectivityController: ConnectivityControllerInstanceOptions; + gasFeeController?: GasFeeControllerInstanceOptions; keyringController?: KeyringControllerInstanceOptions; networkController: NetworkControllerInstanceOptions; remoteFeatureFlagController: RemoteFeatureFlagControllerInstanceOptions; diff --git a/packages/wallet/tsconfig.build.json b/packages/wallet/tsconfig.build.json index cc15d40bc1..eebd0e34a2 100644 --- a/packages/wallet/tsconfig.build.json +++ b/packages/wallet/tsconfig.build.json @@ -12,6 +12,7 @@ { "path": "../base-controller/tsconfig.build.json" }, { "path": "../connectivity-controller/tsconfig.build.json" }, { "path": "../controller-utils/tsconfig.build.json" }, + { "path": "../gas-fee-controller/tsconfig.build.json" }, { "path": "../keyring-controller/tsconfig.build.json" }, { "path": "../messenger/tsconfig.build.json" }, { "path": "../network-controller/tsconfig.build.json" }, diff --git a/packages/wallet/tsconfig.json b/packages/wallet/tsconfig.json index ce310304ec..b878dd8fa3 100644 --- a/packages/wallet/tsconfig.json +++ b/packages/wallet/tsconfig.json @@ -22,6 +22,9 @@ { "path": "../controller-utils" }, + { + "path": "../gas-fee-controller" + }, { "path": "../keyring-controller" }, diff --git a/yarn.lock b/yarn.lock index 91de6e9c44..8f48c15c55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9159,6 +9159,7 @@ __metadata: "@metamask/browser-passworder": "npm:^6.0.0" "@metamask/connectivity-controller": "npm:^0.3.0" "@metamask/controller-utils": "npm:^12.3.0" + "@metamask/gas-fee-controller": "npm:^26.2.4" "@metamask/keyring-controller": "npm:^27.1.0" "@metamask/messenger": "npm:^2.0.0" "@metamask/network-controller": "npm:^34.0.0" From 7ed2770aeee83d379b4f9953b2a411e091d9b9a3 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Wed, 15 Jul 2026 20:10:53 +0200 Subject: [PATCH 2/2] docs(wallet): link changelog entry to PR #9527 Co-Authored-By: Claude Opus 4.8 (1M context) --- packages/wallet/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index d9412ef0a0..3b611a7941 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **BREAKING:** Wire `GasFeeController` into the default wallet initialization ([#9510](https://github.com/MetaMask/core/pull/9510)) +- **BREAKING:** Wire `GasFeeController` into the default wallet initialization ([#9527](https://github.com/MetaMask/core/pull/9527)) ## [7.0.1]