Skip to content

feat(wallet)!: wire GasFeeController into default initialization#9527

Open
sirtimid wants to merge 3 commits into
mainfrom
sirtimid/wire-gas-fee-controller
Open

feat(wallet)!: wire GasFeeController into default initialization#9527
sirtimid wants to merge 3 commits into
mainfrom
sirtimid/wire-gas-fee-controller

Conversation

@sirtimid

@sirtimid sirtimid commented Jul 15, 2026

Copy link
Copy Markdown
Member

Explanation

Wires GasFeeController into @metamask/wallet's default initialization set as its own InitializationConfiguration under src/initialization/instances/gas-fee-controller/, mirroring the most recently merged instances (transaction-controller, network-controller).

TransactionController is already wired and its messenger delegates GasFeeController:fetchGasFeeEstimates. Delegation registers a lazy handler, so the wallet boots fine today — but the first transaction flow that needs gas estimates throws A handler for GasFeeController:fetchGasFeeEstimates has not been registered. Wiring GasFeeController is the missing piece that lets a wired TransactionController actually estimate gas.

@metamask/wallet is the shared controller-integration layer that metamask-extension, metamask-mobile, and @metamask/wallet-cli all adopt, so every value that differs between clients is an injectable instanceOptions.gasFeeController slot with a platform-agnostic default — nothing is baked to one client.

Constructor callbacks

GasFeeController takes direct callbacks rather than pure messenger delegation. The instance builds them from the wired NetworkController:

  • getProviderNetworkController:getState (selectedNetworkClientId) then NetworkController:getNetworkClientById(id).provider
  • getCurrentNetworkEIP1559CompatibilityNetworkController:getEIP1559Compatibility (coerced to a defined boolean)

onNetworkDidChange / getChainId are omitted — the constructor already has a messenger-based network-tracking fallback when both are absent (subscribes NetworkController:networkDidChange).

Injectable options + per-environment values

Option Extension Mobile Default (wallet-cli / package)
EIP1559APIEndpoint .../networks/<chain_id>/suggestedGasFees (dev override → gas.uat-api.cx.metamask.io) same prod URL prod URL; injectable
legacyAPIEndpoint ${GAS_API}/networks/<chain_id>/gasPrices same prod URL prod URL; injectable
clientId 'extension' 'mobile' 'cli' (sent as X-Client-Id); injectable
interval 10_000 15_000 controller default 15_000; injectable
getCurrentNetworkLegacyGasAPICompatibility chainId === BSC mainnet || BSC || POLYGON () => false; injectable
getCurrentAccountEIP1559Compatibility () => true omitted () => true; injectable

Initialization ordering

GasFeeController's constructor eagerly reads NetworkController state (to build its provider), so NetworkController must be constructed first. The default set was previously constructed in the alphabetical order of Object.values(defaultConfigurations), which happened to satisfy every existing dependency (e.g. network < transaction) — but gas < network breaks that, causing a A handler for NetworkController:getState has not been registered throw at construction.

To fix this generally, this PR adds an optional dependencies field to InitializationConfiguration and a stable topological sort (orderByDependencies) in initialize. gasFeeController declares dependencies: ['NetworkController']. Instances without an unmet dependency keep their relative order, and dependencies outside the set are treated as already satisfied.

fetch decision

GasFeeController fetches gas estimates via the global fetch inside @metamask/gas-fee-controller (it has no injectable fetch option). This PR takes path (a): accept the global fetch — no upstream change; works on Node 18+ (wallet-cli daemon), modern browsers, and React Native. The global-fetch usage lives inside the controller package, not inside @metamask/wallet code, and both clients already rely on this today. Adding an injectable fetch option to @metamask/gas-fee-controller is tracked as a possible follow-up if the convention is to be enforced strictly.

Client adoption PRs

References

Related

Checklist

  • I've read the contributing guidelines.
  • I've completed the PR template to the best of my ability.
  • I've included tests if applicable (100% coverage; colocated instance test + orderByDependencies unit test).
  • I've documented my code using JSDoc format if applicable.
  • I've applied the right labels on the PR (see labeling guidelines). Not required for external contributors.

🤖 Generated with Claude Code


Note

Medium Risk
Breaking API for all Wallet consumers and changes core wallet bootstrap order; impact is localized to gas estimation wiring rather than auth or funds movement.

Overview
BREAKING: @metamask/wallet now boots GasFeeController by default so TransactionController gas flows (e.g. GasFeeController:fetchGasFeeEstimates) have a real handler instead of failing at runtime.

Consumers must pass instanceOptions.gasFeeController with a required clientId (gas API X-Client-Id); other gas options stay optional with shared defaults (prod gas API URLs, network callbacks wired from NetworkController).

Wallet init no longer relies on implicit alphabetical construction order: configurations can declare dependencies, and orderByDependencies topologically sorts them (GasFeeController depends on NetworkController). wallet-cli sets clientId: 'cli'.

Reviewed by Cursor Bugbot for commit ec8eeec. Bugbot is set up for automated code reviews on this repo. Configure here.

sirtimid added a commit that referenced this pull request Jul 15, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sirtimid
sirtimid requested a review from a team July 15, 2026 18:13
@sirtimid

Copy link
Copy Markdown
Member Author

@metamaskbot publish-previews

@github-actions

Copy link
Copy Markdown
Contributor

Preview builds have been published. Learn how to use preview builds in other projects.

Expand for full list of packages and versions.
@metamask-previews/account-tree-controller@7.5.5-preview-7ed2770ae
@metamask-previews/accounts-controller@39.0.5-preview-7ed2770ae
@metamask-previews/address-book-controller@7.1.2-preview-7ed2770ae
@metamask-previews/ai-controllers@0.8.0-preview-7ed2770ae
@metamask-previews/analytics-controller@1.2.1-preview-7ed2770ae
@metamask-previews/analytics-data-regulation-controller@0.0.0-preview-7ed2770ae
@metamask-previews/announcement-controller@8.1.0-preview-7ed2770ae
@metamask-previews/app-metadata-controller@2.0.1-preview-7ed2770ae
@metamask-previews/approval-controller@9.0.2-preview-7ed2770ae
@metamask-previews/assets-controller@11.0.0-preview-7ed2770ae
@metamask-previews/assets-controllers@109.4.1-preview-7ed2770ae
@metamask-previews/authenticated-user-storage@3.0.1-preview-7ed2770ae
@metamask-previews/base-controller@9.1.0-preview-7ed2770ae
@metamask-previews/base-data-service@0.1.3-preview-7ed2770ae
@metamask-previews/bitcoin-regtest-up@1.0.0-preview-7ed2770ae
@metamask-previews/bridge-controller@77.5.0-preview-7ed2770ae
@metamask-previews/bridge-status-controller@74.2.0-preview-7ed2770ae
@metamask-previews/build-utils@3.0.4-preview-7ed2770ae
@metamask-previews/chain-agnostic-permission@1.7.0-preview-7ed2770ae
@metamask-previews/chomp-api-service@3.1.0-preview-7ed2770ae
@metamask-previews/claims-controller@0.5.3-preview-7ed2770ae
@metamask-previews/client-controller@1.0.1-preview-7ed2770ae
@metamask-previews/client-utils@1.1.0-preview-7ed2770ae
@metamask-previews/compliance-controller@2.1.0-preview-7ed2770ae
@metamask-previews/composable-controller@12.0.1-preview-7ed2770ae
@metamask-previews/config-registry-controller@0.4.1-preview-7ed2770ae
@metamask-previews/connectivity-controller@0.3.0-preview-7ed2770ae
@metamask-previews/controller-utils@12.3.0-preview-7ed2770ae
@metamask-previews/core-backend@6.5.0-preview-7ed2770ae
@metamask-previews/delegation-controller@3.0.2-preview-7ed2770ae
@metamask-previews/earn-controller@12.2.2-preview-7ed2770ae
@metamask-previews/eip-5792-middleware@3.0.5-preview-7ed2770ae
@metamask-previews/eip-7702-internal-rpc-middleware@0.1.1-preview-7ed2770ae
@metamask-previews/eip1193-permission-middleware@2.0.1-preview-7ed2770ae
@metamask-previews/ens-controller@19.1.5-preview-7ed2770ae
@metamask-previews/eth-block-tracker@15.0.1-preview-7ed2770ae
@metamask-previews/eth-json-rpc-middleware@23.1.3-preview-7ed2770ae
@metamask-previews/eth-json-rpc-provider@6.0.1-preview-7ed2770ae
@metamask-previews/foundryup@1.0.1-preview-7ed2770ae
@metamask-previews/gas-fee-controller@26.2.4-preview-7ed2770ae
@metamask-previews/gator-permissions-controller@4.2.3-preview-7ed2770ae
@metamask-previews/geolocation-controller@0.1.3-preview-7ed2770ae
@metamask-previews/java-tron-up@1.0.0-preview-7ed2770ae
@metamask-previews/json-rpc-engine@10.5.0-preview-7ed2770ae
@metamask-previews/json-rpc-middleware-stream@8.0.8-preview-7ed2770ae
@metamask-previews/keyring-controller@27.1.0-preview-7ed2770ae
@metamask-previews/local-node-utils@1.0.0-preview-7ed2770ae
@metamask-previews/logging-controller@8.0.2-preview-7ed2770ae
@metamask-previews/message-manager@14.1.2-preview-7ed2770ae
@metamask-previews/messenger@2.0.0-preview-7ed2770ae
@metamask-previews/messenger-cli@0.2.0-preview-7ed2770ae
@metamask-previews/money-account-api-data-service@0.1.0-preview-7ed2770ae
@metamask-previews/money-account-balance-service@2.2.0-preview-7ed2770ae
@metamask-previews/money-account-controller@0.3.3-preview-7ed2770ae
@metamask-previews/money-account-upgrade-controller@2.2.1-preview-7ed2770ae
@metamask-previews/multichain-account-service@13.0.0-preview-7ed2770ae
@metamask-previews/multichain-api-middleware@4.0.1-preview-7ed2770ae
@metamask-previews/multichain-network-controller@3.2.1-preview-7ed2770ae
@metamask-previews/multichain-transactions-controller@7.1.1-preview-7ed2770ae
@metamask-previews/name-controller@9.1.2-preview-7ed2770ae
@metamask-previews/network-connection-banner-controller@0.1.0-preview-7ed2770ae
@metamask-previews/network-controller@34.0.0-preview-7ed2770ae
@metamask-previews/network-enablement-controller@5.6.0-preview-7ed2770ae
@metamask-previews/notification-services-controller@25.0.0-preview-7ed2770ae
@metamask-previews/passkey-controller@2.1.0-preview-7ed2770ae
@metamask-previews/permission-controller@13.1.1-preview-7ed2770ae
@metamask-previews/permission-log-controller@5.1.0-preview-7ed2770ae
@metamask-previews/perps-controller@9.2.1-preview-7ed2770ae
@metamask-previews/phishing-controller@17.2.1-preview-7ed2770ae
@metamask-previews/platform-api-docs@0.0.0-preview-7ed2770ae
@metamask-previews/polling-controller@16.0.8-preview-7ed2770ae
@metamask-previews/preferences-controller@23.1.0-preview-7ed2770ae
@metamask-previews/profile-metrics-controller@4.0.2-preview-7ed2770ae
@metamask-previews/profile-sync-controller@28.3.0-preview-7ed2770ae
@metamask-previews/ramps-controller@17.0.0-preview-7ed2770ae
@metamask-previews/rate-limit-controller@7.0.1-preview-7ed2770ae
@metamask-previews/react-data-query@0.2.1-preview-7ed2770ae
@metamask-previews/remote-feature-flag-controller@4.2.2-preview-7ed2770ae
@metamask-previews/sample-controllers@5.0.3-preview-7ed2770ae
@metamask-previews/seedless-onboarding-controller@10.0.3-preview-7ed2770ae
@metamask-previews/selected-network-controller@26.1.5-preview-7ed2770ae
@metamask-previews/sentinel-api-service@1.0.0-preview-7ed2770ae
@metamask-previews/shield-controller@5.1.3-preview-7ed2770ae
@metamask-previews/signature-controller@39.2.7-preview-7ed2770ae
@metamask-previews/smart-transactions-controller@25.0.1-preview-7ed2770ae
@metamask-previews/snap-account-service@2.0.0-preview-7ed2770ae
@metamask-previews/social-controllers@2.5.0-preview-7ed2770ae
@metamask-previews/solana-test-validator-up@1.0.0-preview-7ed2770ae
@metamask-previews/stellar-quickstart-up@0.0.0-preview-7ed2770ae
@metamask-previews/storage-service@1.0.2-preview-7ed2770ae
@metamask-previews/subscription-controller@6.2.1-preview-7ed2770ae
@metamask-previews/transaction-controller@69.0.0-preview-7ed2770ae
@metamask-previews/transaction-pay-controller@25.0.0-preview-7ed2770ae
@metamask-previews/user-operation-controller@41.2.7-preview-7ed2770ae
@metamask-previews/wallet@7.0.1-preview-7ed2770ae
@metamask-previews/wallet-cli@0.0.0-preview-7ed2770ae

sirtimid added a commit to MetaMask/metamask-extension that referenced this pull request Jul 16, 2026
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>
@sirtimid
sirtimid marked this pull request as ready for review July 16, 2026 15:14
@sirtimid
sirtimid requested a review from a team as a code owner July 16, 2026 15:14
@sirtimid
sirtimid temporarily deployed to default-branch July 16, 2026 15:18 — with GitHub Actions Inactive
sirtimid added a commit to MetaMask/metamask-extension that referenced this pull request Jul 16, 2026
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 rekmarks left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a couple of things.

Comment on lines +90 to +96
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;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That error doesn't actually explain what the problem is. Shouldn't we instead throw here and note what the cycle is?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed ec8eeec

// The controller substitutes `<chain_id>` with the decimal chain ID per request.
const DEFAULT_EIP1559_API_ENDPOINT = `${GAS_API_BASE_URL}/networks/<chain_id>/suggestedGasFees`;
const DEFAULT_LEGACY_API_ENDPOINT = `${GAS_API_BASE_URL}/networks/<chain_id>/gasPrices`;
const DEFAULT_CLIENT_ID = 'cli';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we instead make client id required? It would be unfortunate if someone forgot to add this in the production clients.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! I made gasFeeController a required instance options key, not just the clientId field ec8eeec

sirtimid added a commit to MetaMask/metamask-extension that referenced this pull request Jul 20, 2026
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>
sirtimid and others added 3 commits July 20, 2026 17:08
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) <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>
@sirtimid
sirtimid force-pushed the sirtimid/wire-gas-fee-controller branch from 7ed2770 to ec8eeec Compare July 20, 2026 14:27
@sirtimid
sirtimid requested a review from a team as a code owner July 20, 2026 14:27
* @returns The configurations ordered so that dependencies come first.
* @throws If the configurations contain a dependency cycle.
*/
export function orderByDependencies(

@matthewwalsh0 matthewwalsh0 Jul 20, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@matthewwalsh0
matthewwalsh0 self-requested a review July 20, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wire GasFeeController into @metamask/wallet default initialization

3 participants