Skip to content

Wire GasFeeController into @metamask/wallet default initialization #9510

Description

@sirtimid

Summary

Wire GasFeeController into @metamask/wallet's default initialization set, as its own InitializationConfiguration instance under src/initialization/instances/gas-fee-controller/, following the pattern of the most recently merged instances (e.g. transaction-controller, network-controller).

@metamask/wallet is the shared controller-integration layer that metamask-extension, metamask-mobile, and @metamask/wallet-cli all adopt, so any value that differs between clients must be an injectable instanceOptions slot with a platform-agnostic default — never hardcoded to one client's value.

Why

TransactionController is already wired into @metamask/wallet and its messenger delegates GasFeeController:fetchGasFeeEstimates. Delegation registers a lazy handler, so today the wallet boots fine — but the moment a transaction flow needs gas estimates, the call throws A handler for GasFeeController:fetchGasFeeEstimates has not been registered. Wiring GasFeeController is the missing piece that lets a wired TransactionController actually estimate gas, and it also unblocks UserOperationController later.

Current status — unblocked

  • Only controller dependency is NetworkController, which is already wired on main. Nothing upstream blocks this.
  • GasFeeController is not yet in src/initialization/instances/index.ts.
  • Neither client has a @metamask/wallet wallet-init instance-options file for it yet, so there is no existing reference to port from — this PR establishes the pattern (and the client-verification bottleneck applies).

Messenger surface

Exposes (already consumed by TransactionController): GasFeeController:fetchGasFeeEstimates (plus getGasFeeEstimatesAndStartPolling, getTimeEstimate, disableGasFeeEstimates/enableGasFeeEstimates, resetPolling, stopPolling, and :getState / :stateChange).

getMessenger must delegate from the root, per the controller's AllowedActions/AllowedEvents — all NetworkController, all already wired:

  • Actions: NetworkController:getState, NetworkController:getNetworkClientById, NetworkController:getEIP1559Compatibility
  • Events: NetworkController:networkDidChange

Keep the allowlist tight — delegate only these.

Constructor requirements

GasFeeController takes direct callbacks, not pure messenger delegation. The instance init({ messenger, state, options }) builds them from the wired NetworkController messenger actions:

Constructor option Required How the instance supplies it
messenger, state yes from the init args
getProvider: () => ProviderProxy yes NetworkController:getState → selectedNetworkClientId, then NetworkController:getNetworkClientById(id).provider
getCurrentNetworkEIP1559Compatibility: () => Promise<boolean> yes () => messenger.call('NetworkController:getEIP1559Compatibility') ?? false
getCurrentNetworkLegacyGasAPICompatibility: () => boolean yes see options table (client-varying → injectable, with default)
EIP1559APIEndpoint: string yes, no default injectable option (see table)
legacyAPIEndpoint, interval, clientId, getCurrentAccountEIP1559Compatibility no injectable / default
onNetworkDidChange, getChainId no can be omitted — the constructor already has a messenger-based network-tracking fallback (subscribes NetworkController:networkDidChange and reads getState/getNetworkClientById itself) when both are absent

GasFeeController extends StaticIntervalPollingController (a mixin) — nothing extra to wire for that.

Injectable options + per-environment table

Add a GasFeeControllerInstanceOptions in types.ts (typed via indexed access from the upstream options), wire it into InstanceSpecificOptions.gasFeeController, and give each a platform-agnostic default.

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

Note: the API host is effectively the same production endpoint across clients; extension adds a dev-API toggle. Keeping the endpoint injectable (rather than hardcoded) is what lets the dev override and the CLI configuration work.

Open decision — fetch injection (please resolve before implementation)

GasFeeController fetches gas estimates via handleFetch → successfulFetch → the global fetch (packages/controller-utils/src/util.ts). It has no injectable fetch option. This is in tension with the integration guide's rule that data-fetching controllers must inject fetch (the package must run in browser, React Native, and Node.js).

Two paths:

  • (a) Accept the global fetch. No upstream change. Works on Node 18+ (wallet-cli daemon), modern browsers, and React Native (with its fetch polyfill). Note that the global-fetch usage lives inside the @metamask/gas-fee-controller package, not inside @metamask/wallet code — the wallet instance never itself reaches for a global. Both clients already rely on this today.
  • (b) Add an injectable fetch option to @metamask/gas-fee-controller first (thread it through gas-util), then wire and inject it here — aligns with the guide/convention. Larger scope; touches a @MetaMask/confirmations-owned package.

Recommendation: (a) to land the wire, with (b) tracked as a separate follow-up if the convention is to be enforced strictly.

Implementation checklist (per the controller-integration guide)

  • src/initialization/instances/gas-fee-controller/gas-fee-controller.ts — const gasFeeController: InitializationConfiguration<...> with name: 'GasFeeController', tight-allowlist getMessenger, and init building the callbacks above.
  • src/initialization/instances/gas-fee-controller/types.ts — GasFeeControllerInstanceOptions (indexed-access types from the upstream options).
  • Wire gasFeeController?: GasFeeControllerInstanceOptions into InstanceSpecificOptions in src/types.ts.
  • Export from src/initialization/instances/index.ts.
  • .github/CODEOWNERS — assign the new instance directory (trailing slash) to @MetaMask/confirmations (mirrors /packages/gas-fee-controller ownership); run yarn lint:teams.
  • Add @metamask/gas-fee-controller as a dependency; update tsconfig project references + root README dependency graph (yarn lint:fix).
  • Tests (TDD, 100% coverage): colocated unit test for the instance; extend src/Wallet.test.ts if it participates; keep the Anvil integration test green.
  • CHANGELOG.md — ## [Unreleased] / ### Added.
  • Verify: yarn workspace @metamask/wallet run test + build, yarn lint:fix, yarn validate:changelog.
  • Open as a draft PR with the per-environment options table and live main reference links.

Client adoption (follow-ups, one PR each)

After this lands and a @metamask/wallet release includes it:

  • metamask-extension — delete gas-fee-controller-init.ts + its messenger factory, construct via the shared Wallet, pass values through instanceOptions.gasFeeController, resolve via wallet.getInstance('GasFeeController').
  • metamask-mobile — same, deleting its gas-fee-controller-init.ts + messenger factory.
  • @metamask/wallet-cli — add a gasFeeController: { EIP1559APIEndpoint, clientId: 'cli' } slot to the daemon's buildInstanceOptions (global fetch is fine on Node).

References (live main)

Acceptance criteria

  • GasFeeController is in the wallet's default wired set; a constructed Wallet exposes GasFeeController:fetchGasFeeEstimates over the shared bus, resolving the TransactionController delegation.
  • All client-varying values are injectable via instanceOptions.gasFeeController with platform-agnostic defaults; nothing baked to one client.
  • The fetch decision above is recorded.
  • Full verification suite passes; PR opened as draft with the options table + live reference links.

Owning team for the controller package: @MetaMask/confirmations (with @MetaMask/core-platform).

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions