feat(wallet)!: wire GasFeeController into default initialization - #9527
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@metamaskbot publish-previews |
|
Preview builds have been published. Learn how to use preview builds in other projects. Expand for full list of packages and versions. |
Construct the extension's GasFeeController through @metamask/wallet instead of the local messenger-client-init wiring, integrating MetaMask/core#9527. The wallet now owns GasFeeController as an instanceOptions.gasFeeController slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from NetworkController itself and lets the controller default onNetworkDidChange and getChainId via its messenger subscription. The extension supplies only the client-specific overrides (interval, clientId, gas API endpoints, and the BSC legacy gas API compatibility check). - Add wallet-init/instance-options/gas-fee-controller.ts and a minimal wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only NetworkController:getState + getNetworkClientById for the chain-id read). - Wire gasFeeController into wallet-init/initialization.ts. - Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController') and drop the GasFeeControllerInit import + init-map entry. - Delete the local confirmations/gas-fee-controller-init and messengers/gas-fee-controller-messenger (+tests) and their entries in messenger-client-init/messengers/index.ts. GasFeeController stays in controller-list.ts. Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the core#9527 preview builds (7ed2770ae) until a wallet release ships it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Construct the extension's GasFeeController through @metamask/wallet instead of the local messenger-client-init wiring, integrating MetaMask/core#9527. The wallet now owns GasFeeController as an instanceOptions.gasFeeController slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from NetworkController itself and lets the controller default onNetworkDidChange and getChainId via its messenger subscription. The extension supplies only the client-specific overrides (interval, clientId, gas API endpoints, and the BSC legacy gas API compatibility check). - Add wallet-init/instance-options/gas-fee-controller.ts and a minimal wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only NetworkController:getState + getNetworkClientById for the chain-id read). - Wire gasFeeController into wallet-init/initialization.ts. - Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController') and drop the GasFeeControllerInit import + init-map entry. - Delete the local confirmations/gas-fee-controller-init and messengers/gas-fee-controller-messenger (+tests) and their entries in messenger-client-init/messengers/index.ts. GasFeeController stays in controller-list.ts. Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the core#9527 preview builds (7ed2770ae) until a wallet release ships it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rekmarks
left a comment
There was a problem hiding this comment.
Looks good! Just a couple of things.
Construct the extension's GasFeeController through @metamask/wallet instead of the local messenger-client-init wiring, integrating MetaMask/core#9527. The wallet now owns GasFeeController as an instanceOptions.gasFeeController slot: it builds getProvider and getCurrentNetworkEIP1559Compatibility from NetworkController itself and lets the controller default onNetworkDidChange and getChainId via its messenger subscription. The extension supplies only the client-specific overrides (interval, clientId, gas API endpoints, and the BSC legacy gas API compatibility check). - Add wallet-init/instance-options/gas-fee-controller.ts and a minimal wallet-init/messengers/gas-fee-controller-messenger.ts (delegates only NetworkController:getState + getNetworkClientById for the chain-id read). - Wire gasFeeController into wallet-init/initialization.ts. - Resolve this.gasFeeController via this.wallet.getInstance('GasFeeController') and drop the GasFeeControllerInit import + init-map entry. - Delete the local confirmations/gas-fee-controller-init and messengers/gas-fee-controller-messenger (+tests) and their entries in messenger-client-init/messengers/index.ts. GasFeeController stays in controller-list.ts. Preview-pin @metamask/wallet and @metamask/gas-fee-controller to the core#9527 preview builds (7ed2770ae) until a wallet release ships it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback on #9527: - Make `instanceOptions.gasFeeController.clientId` required and drop the `'cli'` default, so every client (extension, mobile, wallet-cli) must identify itself to the gas API rather than silently inheriting a default. `gasFeeController` is now a required instance-options key (mirroring `networkController`); wallet-cli passes `clientId: 'cli'` explicitly. - Throw a descriptive error naming the members of the cycle when `orderByDependencies` cannot satisfy the declared dependencies, instead of silently deferring to a later "handler not registered" messenger error. Also re-add the `gas-fee-controller` instance rule via the new programmatic `codeowners.ts` generator (#9529) picked up in the rebase onto main. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7ed2770 to
ec8eeec
Compare
| * @returns The configurations ordered so that dependencies come first. | ||
| * @throws If the configurations contain a dependency cycle. | ||
| */ | ||
| export function orderByDependencies( |
There was a problem hiding this comment.
This is technically very cool! But logistically terrifying 😅
Should we ensure we don't migrate packages until they are initialisation-order-agnostic?
We had to do this for TransactionController for example.
I'd hope these dependencies are very rare, as we usually rely on legacy callbacks or dynamic messenger calls, rather than hard dependencies at constructor / initialisation time?
There was a problem hiding this comment.
Should we ensure we don't migrate packages until they are initialisation-order-agnostic?
We'll look into what it'd take to do this, but are we not already doing the equivalent of this in the clients?
There was a problem hiding this comment.
@matthewwalsh0 here is the controller change #9569
My review was addressed, holding approval pending resolution of other feedback.
…gnostic (MetaMask#9569) ## Explanation The `GasFeeController` constructor was the only currently-wired-or-wireable controller that performed **eager, construction-time reads** of `NetworkController` and the network provider: - `new EthQuery(this.#getProvider())` — unconditional. - Seeding `currentChainId` — either via an eager `getChainId()` call or via `NetworkController:getState` + `getNetworkClientById`. This forced any shared initialization (e.g. `@metamask/wallet`) to guarantee `NetworkController` is constructed *before* `GasFeeController`, which is exactly the construction-order coupling we want to avoid. Every other order-agnostic controller resolves its collaborators lazily. This PR defers those reads to first use so the controller can be constructed in any order, matching the pattern already used elsewhere. **No public API change** — the constructor signature and gas-fee-fetching behavior are unchanged. ## Changes - Removed the eager `EthQuery` build and the eager `currentChainId` seeding from the constructor. Network-change subscription wiring is kept as-is (subscribing is safe regardless of construction order). - Added lazy, memoized resolution: - `#getEthQuery()` — builds the `EthQuery` from `getProvider` on first use. - `#getCurrentChainId()` — resolves the chain ID from the `getChainId` callback when provided (only when paired with `onNetworkDidChange`, preserving the original coupling), otherwise from `NetworkController`. - `#onNetworkControllerDidChange` now resets `ethQuery` to `undefined` (rebuilt lazily on the next fetch) instead of rebuilding it eagerly per network change, and still updates `currentChainId`. ## Testing - New tests assert construction performs **no** `NetworkController:getState` / `getNetworkClientById` / `getProvider` / `getChainId` calls — even when those handlers throw — for both the callback and no-callback constructor branches. - New test covers network-change handling via both the `onNetworkDidChange` callback and the `NetworkController:networkDidChange` messenger subscription (chain ID + eth query update on the next fetch). - All existing tests pass unchanged (53 total). 100% coverage on the changed lines; remaining uncovered lines are pre-existing. ## References Splits out the root-cause fix from the wiring PR MetaMask#9527, so that `GasFeeController` can be migrated into the shared `@metamask/wallet` init set without a dependency-ordering mechanism. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches core gas-estimation initialization and network-switch caching; behavior is intended to stay the same but timing of first network reads shifts to first fetch. > > **Overview** > **`GasFeeController` no longer touches `NetworkController` or the RPC provider during construction**, so it can be instantiated before the network stack is ready (e.g. shared `@metamask/wallet` init) without changing the public constructor or fetch semantics. > > The constructor drops eager `EthQuery` creation and immediate chain-ID resolution (`getChainId()` / `getState` + `getNetworkClientById`). Instead, **`#getEthQuery()`** and **`#getCurrentChainId()`** memoize provider and chain ID on first use inside **`_fetchGasFeeEstimateData`**. When **`onNetworkDidChange` + `getChainId`** are supplied, the callback is stored without calling `getChainId` at construct time. > > On network change, **`#onNetworkControllerDidChange`** clears the cached `ethQuery` (rebuilt on the next fetch) and still updates `currentChainId` and polling. Tests lock in zero constructor-time network/provider/`getChainId` calls and correct chain ID + provider refresh after network switches via callback or **`NetworkController:networkDidChange`**. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 66c249d. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ec8eeec to
ee084b8
Compare
Wire `GasFeeController` into `@metamask/wallet`'s default initialization set as its own `InitializationConfiguration`, mirroring the `transaction-controller` / `network-controller` instances. A wired `TransactionController` delegates `GasFeeController:fetchGasFeeEstimates` lazily, so the wallet boots today but throws the first time a transaction flow needs gas estimates; wiring `GasFeeController` supplies that handler. Adds a required `instanceOptions.gasFeeController` option whose `clientId` (sent as `X-Client-Id` to the gas API) is required, so every client identifies itself; all other fields are optional and fall back to platform-agnostic defaults. wallet-cli passes `clientId: 'cli'`. The instance builds `getProvider` / `getCurrentNetworkEIP1559Compatibility` as lazy closures over `NetworkController`, and `GasFeeController` was made initialization-order-agnostic upstream (#9569), so no dependency-ordering machinery is needed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ee084b8 to
baa4472
Compare
…etaMask#9612) ## Explanation Makes the `@metamask/wallet-cli` daemon able to process a transaction end-to-end by completing the two consumer-side pieces a wired `TransactionController` needs at runtime. This makes the daemon transaction-*capable*; the user-facing `mm send` command is a separate follow-on. `TransactionController` is wired into `@metamask/wallet` and its messenger delegates to `GasFeeController` and `ApprovalController`. A send flow currently dead-ends in the daemon in two ways, both fixed here. ### Piece 1 — consume `GasFeeController` The `gasFeeController` slot (`clientId: 'cli'`) already exists in `buildInstanceOptions` — it was added when `GasFeeController` was wired upstream (MetaMask#9527) because `clientId` is required. This PR finishes the consumer side: - Documents the slot in the `buildInstanceOptions` JSDoc slot list. - Relies on the wallet package's platform-agnostic **production default** for `EIP1559APIEndpoint` (the default is already the prod URL) rather than re-specifying the string in the CLI, so the endpoint stays centrally owned. With `GasFeeController` wired and released, a daemon-hosted `Wallet` now resolves `GasFeeController:fetchGasFeeEstimates` instead of throwing `A handler for ... has not been registered`. ### Piece 2 — headless auto-approval `ApprovalController:addRequest` is **awaited** by transaction/signature flows. The daemon's `showApprovalRequest` is a no-op (it only signals "a request needs attention"; it does not resolve anything), so with no UI the awaiting call hangs forever. `subscribeToAutoApproval` subscribes to `ApprovalController:stateChanged` and immediately accepts every pending request via `ApprovalController:acceptRequest`. The `showApprovalRequest` hook stays a no-op (there is no UI); the id isn't available to that hook, so acceptance goes through the messenger instead. An `inFlight` guard keeps accepting idempotent across the re-entrant/rapid state changes a single flow emits, and both sync throws and async rejections from an accept are logged and swallowed so one bad request can't crash the daemon or wedge the subscription. The subscription is installed in `createWallet` and torn down in its `dispose` path. ### Security consideration — auto-approval is a conscious trust decision Auto-approval means the daemon **accepts every approval request without confirmation** — transactions and signatures included. For a headless daemon this is the intended model: it is driven only by its owner's local CLI over a `0600`, same-user Unix socket, so the trust boundary is the socket, not a per-request prompt. This is documented as the daemon's explicit trust model in `subscribeToAutoApproval` and the README, and flagged as **not "safe by default"** — a scoped/opt-in policy (config flag, or accepting only specific approval types) is deferred until the user-facing send command exists. ## References - `packages/wallet-cli/src/daemon/auto-approval.ts` — the auto-approval subscription + trust model. - `packages/wallet-cli/src/daemon/wallet-factory.ts` — `buildInstanceOptions` slot docs; `createWallet`/`teardown` wiring. ## Related - Closes MetaMask#9512 - Builds on MetaMask#9509 (TransactionController slot) and MetaMask#9527 (wire `GasFeeController` into `@metamask/wallet`) - Unblocks MetaMask#9513 (the `mm` send command), which is additionally gated on MetaMask#9511 ## Checklist - [x] Tests cover both the gas slot and the auto-approval accept path (unit + real-`Wallet` integration); 100% coverage maintained. - [x] `build`, package `test`, `yarn lint:fix`, `yarn lint`, `changelog:validate` pass. - [x] Auto-approval trust model documented in code and README. - [x] Teardown (`dispose`) unsubscribes the auto-approval listener. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > The daemon unconditionally approves transactions and signatures with no per-request prompt; compromise of the local Unix socket trust boundary can move funds. > > **Overview** > Enables end-to-end transaction capability in the headless wallet daemon by **auto-accepting every pending `ApprovalController` request** via a new `subscribeToAutoApproval` subscription on `ApprovalController:stateChanged`, so awaited `addRequest` calls no longer hang behind the no-op `showApprovalRequest` hook. Acceptance uses an **in-flight guard** and logs/swallows accept failures so re-entrant state updates and bad requests cannot wedge or crash the daemon. > > `createWallet` installs the subscription before `wallet.init` and tears it down in `dispose` (shared `runUnsubscribe` helper alongside persistence). **README and changelog** document the explicit security model: anything on the `0600` same-user socket can move funds; scoped approval policy is deferred. > > Also expands **`buildInstanceOptions` JSDoc** for the existing `gasFeeController` slot (`clientId: 'cli'`, relying on wallet defaults for the production gas API URL). > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 26e4183. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…metamask/wallet (MetaMask#33430) ## **Description** Bumps `@metamask/wallet` to **v8.1.0** and adopts the two controllers it newly wires into the default `Wallet` initialization set, continuing the shared controller-integration migration ([`@metamask/wallet`](https://github.com/MetaMask/core/tree/main/packages/wallet) is the layer that extension, mobile, and wallet-cli all share). **GasFeeController** (core [MetaMask#9527](MetaMask/core#9527)): - Adds `wallet-init/instance-options/gas-fee-controller.ts` (+ test) supplying the mobile-specific options: `clientId`, EIP-1559/legacy gas API endpoints, and `getCurrentNetworkLegacyGasAPICompatibility` (mainnet ‖ BSC ‖ Polygon). The wallet builds `getProvider` and `getCurrentNetworkEIP1559Compatibility` from `NetworkController` itself. - Deletes the local `gas-fee-controller-init` and `gas-fee-controller-messenger` (+ tests + barrels). - Resolves the instance via `this.#wallet.getInstance('GasFeeController')` in `Engine.ts`. **SeedlessOnboardingController** (core [MetaMask#9533](MetaMask/core#9533)) — required in the same PR because wallet@8 wires both; leaving the local init would cause a double-registration boot crash: - Adds `wallet-init/instance-options/seedless-onboarding-controller.ts` (+ test) supplying the mobile-specific options: encryptor `cipher`↔`data` adapter, JWT handlers via `AuthTokenHandler`, `web3AuthNetwork`, and `passwordOutdatedCacheTTL`. - Deletes the local `seedless-onboarding-controller-messenger` factory entry from `MESSENGER_FACTORIES`. - Updates E2E Metro mock to intercept `@metamask/seedless-onboarding-controller` at the **package** level (routing to `tests/module-mocking/seedless/package.ts`) instead of the former local controller path. - Resolves the instance via `this.#wallet.getInstance('SeedlessOnboardingController')` in `Engine.ts`. Both controllers are dropped from `MessengerClientsToInitialize` only; they remain in `MessengerClients` / `EngineState` / actions / events / `BACKGROUND_STATE_CHANGE_EVENT_NAMES`, matching the already-migrated controllers. ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: MetaMask/core#9527 Refs: MetaMask/core#9533 ## **Manual testing steps** 1. Build and launch the app on iOS or Android and confirm it boots with no `A handler for GasFeeController:* has already been registered` or `SeedlessOnboardingController:*` error. 2. Open the send/confirmation flow on an EIP-1559 network (e.g. Ethereum mainnet) and confirm gas fee estimates (low / market / aggressive) load and update. 3. Repeat on a legacy-gas network (e.g. BSC or Polygon) and confirm gas prices load. 4. Submit a transaction and confirm gas estimation drives the fee correctly. 5. On a build with seedless onboarding enabled, complete a social login flow and confirm vault encryption/decryption works end-to-end. ## **Screenshots/Recordings** N/A — internal controller-wiring refactor; no UI changes. ### **Before** <!-- N/A --> ### **After** <!-- N/A --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > Touches gas estimation for sends/confirmations and seedless vault encryption/onboarding—both security- and funds-sensitive—and changes controller lifecycle in a way that can cause boot failures if registration diverges. > > **Overview** > Bumps **`@metamask/wallet`** to **v8.1.0** (with **`@metamask/gas-fee-controller`** and **`@metamask/seedless-onboarding-controller`** bumps) and moves **GasFeeController** and **SeedlessOnboardingController** off mobile’s Engine messenger-client init path into the shared Wallet bootstrap. > > **GasFeeController** and **SeedlessOnboardingController** are no longer registered via local `*-init` modules or messenger factories. Mobile passes **`gasFeeController`** and **`seedlessOnboardingController`** **`instanceOptions`** from new **`wallet-init/instance-options/*`** builders (gas API endpoints, legacy-gas compatibility, seedless encryptor adapter, JWT handlers, Web3Auth network). **`Engine`** resolves both with **`this.#wallet.getInstance(...)`** instead of **`messengerClientsByName`**, avoiding double registration at boot. > > Local **`gas-fee-controller-init`**, **`gas-fee-controller-messenger`**, and the old seedless init/messenger wiring are removed; tests move to the instance-option modules. **CODEOWNERS** and E2E **Metro** mocks shift to **`@metamask/seedless-onboarding-controller`** at the package level because Wallet now constructs that controller. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 82f1fe5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com>
…metamask/wallet (#33430) ## **Description** Bumps `@metamask/wallet` to **v8.1.0** and adopts the two controllers it newly wires into the default `Wallet` initialization set, continuing the shared controller-integration migration ([`@metamask/wallet`](https://github.com/MetaMask/core/tree/main/packages/wallet) is the layer that extension, mobile, and wallet-cli all share). **GasFeeController** (core [#9527](MetaMask/core#9527)): - Adds `wallet-init/instance-options/gas-fee-controller.ts` (+ test) supplying the mobile-specific options: `clientId`, EIP-1559/legacy gas API endpoints, and `getCurrentNetworkLegacyGasAPICompatibility` (mainnet ‖ BSC ‖ Polygon). The wallet builds `getProvider` and `getCurrentNetworkEIP1559Compatibility` from `NetworkController` itself. - Deletes the local `gas-fee-controller-init` and `gas-fee-controller-messenger` (+ tests + barrels). - Resolves the instance via `this.#wallet.getInstance('GasFeeController')` in `Engine.ts`. **SeedlessOnboardingController** (core [#9533](MetaMask/core#9533)) — required in the same PR because wallet@8 wires both; leaving the local init would cause a double-registration boot crash: - Adds `wallet-init/instance-options/seedless-onboarding-controller.ts` (+ test) supplying the mobile-specific options: encryptor `cipher`↔`data` adapter, JWT handlers via `AuthTokenHandler`, `web3AuthNetwork`, and `passwordOutdatedCacheTTL`. - Deletes the local `seedless-onboarding-controller-messenger` factory entry from `MESSENGER_FACTORIES`. - Updates E2E Metro mock to intercept `@metamask/seedless-onboarding-controller` at the **package** level (routing to `tests/module-mocking/seedless/package.ts`) instead of the former local controller path. - Resolves the instance via `this.#wallet.getInstance('SeedlessOnboardingController')` in `Engine.ts`. Both controllers are dropped from `MessengerClientsToInitialize` only; they remain in `MessengerClients` / `EngineState` / actions / events / `BACKGROUND_STATE_CHANGE_EVENT_NAMES`, matching the already-migrated controllers. ## **Changelog** CHANGELOG entry: null ## **Related issues** Refs: MetaMask/core#9527 Refs: MetaMask/core#9533 ## **Manual testing steps** 1. Build and launch the app on iOS or Android and confirm it boots with no `A handler for GasFeeController:* has already been registered` or `SeedlessOnboardingController:*` error. 2. Open the send/confirmation flow on an EIP-1559 network (e.g. Ethereum mainnet) and confirm gas fee estimates (low / market / aggressive) load and update. 3. Repeat on a legacy-gas network (e.g. BSC or Polygon) and confirm gas prices load. 4. Submit a transaction and confirm gas estimation drives the fee correctly. 5. On a build with seedless onboarding enabled, complete a social login flow and confirm vault encryption/decryption works end-to-end. ## **Screenshots/Recordings** N/A — internal controller-wiring refactor; no UI changes. ### **Before** <!-- N/A --> ### **After** <!-- N/A --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **High Risk** > Touches gas estimation for sends/confirmations and seedless vault encryption/onboarding—both security- and funds-sensitive—and changes controller lifecycle in a way that can cause boot failures if registration diverges. > > **Overview** > Bumps **`@metamask/wallet`** to **v8.1.0** (with **`@metamask/gas-fee-controller`** and **`@metamask/seedless-onboarding-controller`** bumps) and moves **GasFeeController** and **SeedlessOnboardingController** off mobile’s Engine messenger-client init path into the shared Wallet bootstrap. > > **GasFeeController** and **SeedlessOnboardingController** are no longer registered via local `*-init` modules or messenger factories. Mobile passes **`gasFeeController`** and **`seedlessOnboardingController`** **`instanceOptions`** from new **`wallet-init/instance-options/*`** builders (gas API endpoints, legacy-gas compatibility, seedless encryptor adapter, JWT handlers, Web3Auth network). **`Engine`** resolves both with **`this.#wallet.getInstance(...)`** instead of **`messengerClientsByName`**, avoiding double registration at boot. > > Local **`gas-fee-controller-init`**, **`gas-fee-controller-messenger`**, and the old seedless init/messenger wiring are removed; tests move to the instance-option modules. **CODEOWNERS** and E2E **Metro** mocks shift to **`@metamask/seedless-onboarding-controller`** at the package level because Wallet now constructs that controller. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 82f1fe5. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com>
Rebasing #9300 onto `main` after five weeks of drift required these adjustments, none of which change the feature being wired: - `codeowners.ts` is now the source of truth for `.github/CODEOWNERS`, which is generated. `initializationPath` accepts an array so `permission-controller` can claim both of the instance directories it supplies, and rules are sorted by pattern so the generated section stays alphabetical. - `Wallet` tests pass the `gasFeeController` instance option, required since #9527. - Regenerate `README.md` and both `tsconfig`s via their fixers. - Repair the `[6.0.0]` changelog section, which a conflict resolution had left with duplicated content. - Spell out the client-facing consequences of the wiring in the changelog: the duplicate-registration collision for consumers passing their own messenger, and the fact that permission specifications with side effects (the Snaps specifications reach `SnapController:*`) need a wider delegation allowlist than the default configuration provides. - Cover `caveatSpecifications` injection, the one instance option without a behavioural assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Explanation
Wires
GasFeeControllerinto@metamask/wallet's default initialization set as its ownInitializationConfigurationundersrc/initialization/instances/gas-fee-controller/, mirroring the most recently merged instances (transaction-controller,network-controller).TransactionControlleris already wired and its messenger delegatesGasFeeController:fetchGasFeeEstimates. Delegation registers a lazy handler, so the wallet boots fine today — but the first transaction flow that needs gas estimates throwsA handler for GasFeeController:fetchGasFeeEstimates has not been registered. WiringGasFeeControlleris the missing piece that lets a wiredTransactionControlleractually estimate gas.@metamask/walletis the shared controller-integration layer thatmetamask-extension,metamask-mobile, and@metamask/wallet-cliall adopt, so every value that differs between clients is an injectableinstanceOptions.gasFeeControllerslot with a platform-agnostic default — nothing is baked to one client.Constructor callbacks
GasFeeControllertakes direct callbacks rather than pure messenger delegation. The instance builds them from the wiredNetworkController:getProvider→NetworkController:getState(selectedNetworkClientId) thenNetworkController:getNetworkClientById(id).providergetCurrentNetworkEIP1559Compatibility→NetworkController:getEIP1559Compatibility(coerced to a definedboolean)onNetworkDidChange/getChainIdare omitted — the constructor already has a messenger-based network-tracking fallback when both are absent (subscribesNetworkController:networkDidChange).Injectable options + per-environment values
EIP1559APIEndpoint.../networks/<chain_id>/suggestedGasFees(dev override →gas.uat-api.cx.metamask.io)legacyAPIEndpoint${GAS_API}/networks/<chain_id>/gasPricesclientId'extension''mobile''cli'(sent asX-Client-Id); injectableinterval10_00015_00015_000; injectablegetCurrentNetworkLegacyGasAPICompatibilitychainId === BSCmainnet || BSC || POLYGON() => false; injectablegetCurrentAccountEIP1559Compatibility() => true() => true; injectableInitialization ordering
No construction-ordering machinery is needed.
GasFeeControllerwas made initialization-order-agnostic upstream (#9569): the constructor no longer eagerly resolvesNetworkController(thegetProvider/getCurrentNetworkEIP1559Compatibilitycallbacks resolve it lazily at call time), so it can be constructed beforeNetworkController.initializetherefore constructs the default set in its natural order, and thedependenciesfield /orderByDependenciessort that an earlier revision of this PR added were dropped in favor of that upstream refactor.fetchdecisionGasFeeControllerfetches gas estimates via the globalfetchinside@metamask/gas-fee-controller(it has no injectablefetchoption). This PR takes path (a): accept the globalfetch— no upstream change; works on Node 18+ (wallet-cli daemon), modern browsers, and React Native. The global-fetchusage lives inside the controller package, not inside@metamask/walletcode, and both clients already rely on this today. Adding an injectablefetchoption to@metamask/gas-fee-controlleris tracked as a possible follow-up if the convention is to be enforced strictly.Client adoption PRs
References
packages/gas-fee-controller/src/GasFeeController.tspackages/wallet/src/initialization/instances/transaction-controller/app/scripts/messenger-client-init/confirmations/gas-fee-controller-init.tsapp/core/Engine/controllers/gas-fee-controller/gas-fee-controller-init.tsRelated
GasFeeControllerinto@metamask/walletdefault initialization #9510Checklist
🤖 Generated with Claude Code
Note
Medium Risk
Breaking API for all Wallet consumers and changes the transaction gas-estimation path, but the integration follows existing controller init patterns and includes dedicated tests.
Overview
Breaking:
@metamask/walletnow bootsGasFeeControlleras part of default initialization, so wiredTransactionControllerflows can callGasFeeController:fetchGasFeeEstimatesinstead of failing with a missing handler.Consumers must pass
instanceOptions.gasFeeControllerwith a requiredclientId(sent asX-Client-Idto the gas API); other gas settings stay optional with shared defaults (production API URLs, network callbacks built fromNetworkController).@metamask/wallet-clisetsclientId: 'cli'on the daemon wallet. Extension/mobile adoption is expected in separate PRs.Reviewed by Cursor Bugbot for commit baa4472. Bugbot is set up for automated code reviews on this repo. Configure here.