Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,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/passkey-controller/ @MetaMask/web3auth
/packages/wallet/src/initialization/instances/remote-feature-flag-controller/ @MetaMask/extension-platform @MetaMask/mobile-platform @MetaMask/core-platform
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,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;
Expand Down
1 change: 1 addition & 0 deletions codeowners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ const PACKAGES: Record<string, PackageInfo> = {
},
'gas-fee-controller': {
teams: ['@MetaMask/confirmations'],
initializationPath: 'gas-fee-controller',
},
'gator-permissions-controller': {
teams: ['@MetaMask/delegation'],
Expand Down
1 change: 1 addition & 0 deletions packages/wallet-cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Wire the `transactionController` slot in the daemon wallet's instance options, so the daemon runs the `TransactionController` with an explicit CLI-appropriate configuration (swaps processing disabled, no client hooks) rather than relying on the controller's implicit defaults ([#9509](https://github.com/MetaMask/core/pull/9509))
- Wire the `gasFeeController` slot in the daemon wallet's instance options, passing `clientId: 'cli'` so the CLI identifies itself to the gas estimation API, now that `@metamask/wallet` requires this option ([#9527](https://github.com/MetaMask/core/pull/9527))
- Add the `mm wallet unlock` command, which dispatches `KeyringController:submitPassword` over the daemon socket, allowing the keyring to be unlocked after a daemon start with no password or after a `mm daemon call KeyringController:setLocked` ([#8821](https://github.com/MetaMask/core/pull/8821))
- Add the `mm daemon list` command, which prints the messenger actions the running daemon can dispatch via `daemon call`, enumerated from the live messenger so the list cannot drift from what `call` accepts ([#9339](https://github.com/MetaMask/core/pull/9339))
- Add the `mm daemon` command suite (`start`, `stop`, `status`, `purge`, and `call`) for running the wallet daemon and dispatching messenger actions over its socket ([#9255](https://github.com/MetaMask/core/pull/9255))
Expand Down
1 change: 1 addition & 0 deletions packages/wallet-cli/src/daemon/wallet-factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ describe('createWallet', () => {
expect(
instanceOptions.connectivityController.connectivityAdapter,
).toBeInstanceOf(AlwaysOnlineAdapter);
expect(instanceOptions.gasFeeController.clientId).toBe('cli');
expect(instanceOptions.networkController.infuraProjectId).toBe(
'test-infura-id',
);
Expand Down
5 changes: 5 additions & 0 deletions packages/wallet-cli/src/daemon/wallet-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ function buildInstanceOptions(
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
// Identifies the CLI to the gas estimation API via the `X-Client-Id`
// header.
clientId: 'cli',
},
networkController: {
infuraProjectId,
},
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **BREAKING:** Wire `GasFeeController` into the default wallet initialization ([#9527](https://github.com/MetaMask/core/pull/9527))
- 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.
- **BREAKING** Wire `SeedlessOnboardingController` and `PasskeyController` into the default wallet initialization ([#9533](https://github.com/MetaMask/core/pull/9533))

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 24 additions & 0 deletions packages/wallet/src/Wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ async function setupWallet(): Promise<Wallet> {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
clientId: 'test',
},
networkController: {
infuraProjectId: 'fake-infura-project-id',
},
Expand Down Expand Up @@ -89,6 +92,9 @@ describe('Wallet', () => {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
clientId: 'test',
},
keyringController: {
encryptor: new MockEncryptor(),
},
Expand Down Expand Up @@ -141,6 +147,9 @@ describe('Wallet', () => {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
clientId: 'test',
},
networkController: {
infuraProjectId: 'fake-infura-project-id',
},
Expand Down Expand Up @@ -184,6 +193,9 @@ describe('Wallet', () => {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
clientId: 'test',
},
networkController: {
infuraProjectId: 'fake-infura-project-id',
},
Expand Down Expand Up @@ -294,6 +306,9 @@ describe('Wallet', () => {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
clientId: 'test',
},
networkController: {
infuraProjectId: 'fake-infura-project-id',
},
Expand Down Expand Up @@ -326,6 +341,9 @@ describe('Wallet', () => {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
clientId: 'test',
},
networkController: {
infuraProjectId: 'fake-infura-project-id',
},
Expand Down Expand Up @@ -366,6 +384,9 @@ describe('Wallet', () => {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
clientId: 'test',
},
networkController: {
infuraProjectId: 'fake-infura-project-id',
},
Expand Down Expand Up @@ -454,6 +475,9 @@ describe('Wallet', () => {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
gasFeeController: {
clientId: 'test',
},
networkController: {
infuraProjectId: 'fake-infura-project-id',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
import { GasFeeController } from '@metamask/gas-fee-controller';
import { Messenger } from '@metamask/messenger';
import { InMemoryStorageAdapter } from '@metamask/storage-service';

import type { WalletOptions } from '../../../types.js';
import { Wallet } from '../../../Wallet.js';
import { defaultConfigurations } from '../../defaults.js';
import type {
DefaultActions,
DefaultEvents,
RootMessenger,
} from '../../defaults.js';
import { AlwaysOnlineAdapter } from '../connectivity-controller/always-online-adapter.js';
import { gasFeeController } from './gas-fee-controller.js';

const controllers: GasFeeController[] = [];
const wallets: Wallet[] = [];

const REMOTE_FEATURE_FLAG_OPTIONS = {
clientConfigApiService: {
fetchRemoteFeatureFlags: async (): Promise<{
remoteFeatureFlags: Record<string, boolean>;
cacheTimestamp: number;
}> => ({ remoteFeatureFlags: {}, cacheTimestamp: Date.now() }),
},
};

type ActionHandler = (...args: unknown[]) => unknown;

type AnyMessenger = Messenger<string>;

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: { clientId: 'test' },
});
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: { clientId: 'test' },
});
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: { clientId: 'test' },
});
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/<chain_id>/eip1559',
legacyAPIEndpoint: 'https://example.test/<chain_id>/legacy',
clientId: 'test-client',
interval: 30_000,
getCurrentAccountEIP1559Compatibility,
getCurrentNetworkLegacyGasAPICompatibility,
},
});
controllers.push(instance);

expect(instance).toBeInstanceOf(GasFeeController);
});
});

function getRootMessenger(): RootMessenger<DefaultActions, DefaultEvents> {
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(),
},
gasFeeController: {
clientId: 'test',
},
networkController: {
infuraProjectId: 'test-infura-project-id',
},
storageService: {
storage: new InMemoryStorageAdapter(),
},
remoteFeatureFlagController: REMOTE_FEATURE_FLAG_OPTIONS,
};
}

function registerActionHandler(
parent: RootMessenger<DefaultActions, DefaultEvents>,
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);
}
Loading