Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6ee1499
feat(wallet): add AccountsController and ConnectivityController
grypez May 28, 2026
e744c91
chore(wallet): add changelog entries for AccountsController and Conne…
grypez May 28, 2026
e85be4e
test(wallet): add tests for AccountsController and ConnectivityContro…
grypez May 28, 2026
a9e2a08
refactor(wallet): simplify AccountsController messenger instantiation
grypez Jun 1, 2026
17ebad6
feat(wallet): make ConnectivityController adapter injectable and call…
grypez Jun 1, 2026
135e595
test(wallet): update ConnectivityController test to reflect factory-f…
grypez Jun 1, 2026
2286e06
test(wallet): use setupWallet in AccountsController test
grypez Jun 1, 2026
7bdf8fd
test(wallet): consolidate utilities tests into Wallet.test.ts
grypez Jun 1, 2026
5f7cd5f
feat(wallet): don't call ConnectivityController.init() from factory
grypez Jun 1, 2026
3efac13
deps(wallet): add accounts-controller and connectivity-controller as …
grypez Jun 1, 2026
b1364f7
docs: Update README.md with package dep links
grypez Jun 1, 2026
784be65
chore: add CODEOWNERS rule for accounts-controller wallet initialization
grypez Jun 1, 2026
af010be
chore: explicitly own wallet initialization instances subpath in CODE…
grypez Jun 1, 2026
b701b1e
refactor(wallet): remove explicit Messenger type params from Connecti…
grypez Jun 1, 2026
eb4d185
fix(accounts-controller): make state optional in constructor
grypez Jun 1, 2026
29825d5
refactor(wallet): drop AccountsController state cast and TODO comment
grypez Jun 1, 2026
7cb173f
refactor(wallet): require connectivityAdapter when connectivityContro…
grypez Jun 1, 2026
878145e
refactor(wallet): require connectivityAdapter as a top-level WalletOp…
grypez Jun 1, 2026
087aa71
test(wallet): pass AlwaysOnlineAdapter explicitly in wallet tests
grypez Jun 1, 2026
af8e906
Merge origin/main into grypez/wallet-account-controllers
grypez Jun 1, 2026
9879a05
fix: lint
grypez Jun 1, 2026
0bc503f
refactor(wallet): remove createSecretRecoveryPhrase utility
grypez Jun 2, 2026
488b4c6
chore(wallet): mark connectivityAdapter requirement as breaking in ch…
grypez Jun 2, 2026
367e85d
refactor(wallet): move connectivityAdapter into instanceOptions
grypez Jun 2, 2026
4c48cc7
chore: mirror accounts-controller ownership for wallet init file
grypez Jun 2, 2026
57c3bab
Merge remote-tracking branch 'origin/main' into grypez/wallet-account…
grypez Jun 2, 2026
f450e56
chore(accounts-controller): add changelog entry for optional construc…
grypez Jun 2, 2026
9a025c4
Merge remote-tracking branch 'origin/main' into grypez/wallet-account…
grypez Jun 2, 2026
02b1f32
refactor(wallet): migrate accounts and connectivity instances to per-…
grypez Jun 2, 2026
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
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@
/packages/profile-metrics-controller @MetaMask/mobile-platform @MetaMask/extension-platform

## Initialization
/packages/wallet/src/initialization/instances/accounts-controller/ @MetaMask/accounts-engineers
/packages/wallet/src/initialization/instances/approval-controller/ @MetaMask/confirmations
/packages/wallet/src/initialization/instances/connectivity-controller/ @MetaMask/core-platform
/packages/wallet/src/initialization/instances/keyring-controller.ts @MetaMask/accounts-engineers @MetaMask/core-platform

## Package Release related
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -567,8 +567,10 @@ linkStyle default opacity:0.5
user_operation_controller --> polling_controller;
user_operation_controller --> transaction_controller;
user_operation_controller --> eth_block_tracker;
wallet --> accounts_controller;
wallet --> approval_controller;
wallet --> base_controller;
wallet --> connectivity_controller;
wallet --> controller_utils;
wallet --> keyring_controller;
wallet --> messenger;
Expand Down
4 changes: 4 additions & 0 deletions packages/accounts-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- The `state` option of the `AccountsController` constructor is now optional, defaulting to the controller's default state when omitted ([#8924](https://github.com/MetaMask/core/pull/8924))

## [38.1.2]

### Changed
Expand Down
2 changes: 1 addition & 1 deletion packages/accounts-controller/src/AccountsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ export class AccountsController extends BaseController<
state,
}: {
messenger: AccountsControllerMessenger;
state: AccountsControllerState;
state?: AccountsControllerState;
}) {
const accountIdByAddress = constructAccountIdByAddress(
state?.internalAccounts?.accounts ?? {},
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:** Add `AccountsController` and `ConnectivityController` as default initialized controllers ([#8924](https://github.com/MetaMask/core/pull/8924))
- Passing `instanceOptions.connectivityController.connectivityAdapter` is now required.
- **BREAKING:** Wire `ApprovalController` into the default wallet initialization ([#8953](https://github.com/MetaMask/core/pull/8953))
- The default `Wallet` now constructs an `ApprovalController` and registers its `ApprovalController:*` messenger actions. Consumers that pass their own `messenger` and already wire an `ApprovalController` must remove their own before upgrading, or the duplicate registration will collide.
- Adds an `approvalController` slot to `instanceOptions` with `showApprovalRequest` (the callback that surfaces pending approval requests to the user; defaults to a no-op) and `typesExcludedFromRateLimiting` (the approval types exempt from per-origin rate limiting; defaults to a baseline of EVM approval types). Both let consumers (extension, mobile, wallet-cli) inject their platform-specific values.
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,11 @@
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"dependencies": {
"@metamask/accounts-controller": "^38.1.2",
"@metamask/approval-controller": "^9.0.1",
"@metamask/base-controller": "^9.1.0",
"@metamask/browser-passworder": "^6.0.0",
"@metamask/connectivity-controller": "^0.2.0",
"@metamask/controller-utils": "^12.1.0",
"@metamask/keyring-controller": "^26.0.0",
"@metamask/messenger": "^1.2.0",
Expand Down
59 changes: 58 additions & 1 deletion packages/wallet/src/Wallet.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller';
import { Messenger } from '@metamask/messenger';
import { InMemoryStorageAdapter } from '@metamask/storage-service';
import { Json } from '@metamask/utils';
import { webcrypto } from 'crypto';

import MockEncryptor from '../../keyring-controller/tests/mocks/mockEncryptor';
import * as initializationModule from './initialization/initialization';
import { AlwaysOnlineAdapter } from './initialization/instances/connectivity-controller/always-online-adapter';
import { importSecretRecoveryPhrase } from './utilities';
import { Wallet } from './Wallet';

Expand All @@ -14,6 +16,9 @@ const TEST_PASSWORD = 'testpass';
async function setupWallet(): Promise<Wallet> {
const wallet = new Wallet({
instanceOptions: {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
storageService: {
storage: new InMemoryStorageAdapter(),
},
Expand Down Expand Up @@ -67,6 +72,9 @@ describe('Wallet', () => {
it('supports passing instance options', async () => {
const wallet = new Wallet({
instanceOptions: {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
keyringController: {
encryptor: new MockEncryptor(),
},
Expand Down Expand Up @@ -112,6 +120,9 @@ describe('Wallet', () => {
},
],
instanceOptions: {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
storageService: {
storage: new InMemoryStorageAdapter(),
},
Expand Down Expand Up @@ -146,7 +157,16 @@ describe('Wallet', () => {
NoMeta: { state: {} },
});

const wallet = new Wallet({});
const wallet = new Wallet({
instanceOptions: {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
storageService: {
storage: new InMemoryStorageAdapter(),
},
},
});

expect(wallet.controllerMetadata).toStrictEqual({
WithMeta: fakeMetadata,
Expand Down Expand Up @@ -191,6 +211,40 @@ describe('Wallet', () => {
expect(spy).toHaveBeenCalledTimes(1);
});

describe('AccountsController', () => {
it('tracks accounts created via KeyringController', async () => {
const wallet = await setupWallet();

const keyringAccounts = await wallet.messenger.call(
'KeyringController:getAccounts',
);
const trackedAddresses = Object.values(
wallet.state.AccountsController.internalAccounts.accounts,
).map((account) => account.address);

expect(trackedAddresses).toStrictEqual(keyringAccounts);
});
});

describe('ConnectivityController', () => {
it('reports online connectivity status', () => {
const wallet = new Wallet({
instanceOptions: {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
storageService: {
storage: new InMemoryStorageAdapter(),
},
},
});

expect(wallet.state.ConnectivityController.connectivityStatus).toBe(
CONNECTIVITY_STATUSES.Online,
);
});
});

describe('KeyringController', () => {
it('can unlock and populate accounts', async () => {
const wallet = await setupWallet();
Expand All @@ -212,6 +266,9 @@ describe('Wallet', () => {
},
},
instanceOptions: {
connectivityController: {
connectivityAdapter: new AlwaysOnlineAdapter(),
},
storageService: {
storage: new InMemoryStorageAdapter(),
},
Expand Down
Copy link
Copy Markdown
Member

@FrederikBolding FrederikBolding Jun 1, 2026

Choose a reason for hiding this comment

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

Let's add a CODEOWNER rule for this that matches the controller (same applies to ConnectivityController).

Additionally, let's have the accounts team review the addition.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {
AccountsController,
AccountsControllerMessenger,
} from '@metamask/accounts-controller';
Comment thread
cursor[bot] marked this conversation as resolved.
import { Messenger } from '@metamask/messenger';

import type {
DefaultActions,
DefaultEvents,
RootMessenger,
} from '../../defaults';
import type { InitializationConfiguration } from '../../types';

export const accountsController: InitializationConfiguration<
AccountsController,
AccountsControllerMessenger
> = {
name: 'AccountsController',
init: ({ state, messenger }) =>
new AccountsController({
state,
messenger,
}),
getMessenger: (parent: RootMessenger<DefaultActions, DefaultEvents>) => {
const accountsControllerMessenger: AccountsControllerMessenger =
new Messenger({
namespace: 'AccountsController',
parent,
});

parent.delegate({
messenger: accountsControllerMessenger,
actions: [
'KeyringController:getState',
'KeyringController:getKeyringsByType',
],
events: [
// AccountsController subscribes to :stateChange internally; the
// delegation must match until that package migrates to :stateChanged.
// eslint-disable-next-line no-restricted-syntax
'KeyringController:stateChange',
'SnapKeyring:accountAssetListUpdated',
'SnapKeyring:accountBalancesUpdated',
'SnapKeyring:accountTransactionsUpdated',
'MultichainNetworkController:networkDidChange',
],
});

return accountsControllerMessenger;
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller';

import { AlwaysOnlineAdapter } from './always-online-adapter';

describe('AlwaysOnlineAdapter', () => {
it('returns Online from getStatus', async () => {
const adapter = new AlwaysOnlineAdapter();
const status = await adapter.getStatus();

expect(status).toBe(CONNECTIVITY_STATUSES.Online);
});

it('onConnectivityChange is a no-op', () => {
const adapter = new AlwaysOnlineAdapter();
const callback = jest.fn();

adapter.onConnectivityChange(callback);

expect(callback).not.toHaveBeenCalled();
});

it('destroy is a no-op', () => {
const adapter = new AlwaysOnlineAdapter();

expect(() => adapter.destroy()).not.toThrow();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
CONNECTIVITY_STATUSES,
ConnectivityAdapter,
ConnectivityStatus,
} from '@metamask/connectivity-controller';

/**
* A connectivity adapter that unconditionally reports the device as online.
*
* This is a temporary placeholder until a real platform-specific adapter
* (one that observes actual network events) is injected by the consumer.
*/
export class AlwaysOnlineAdapter implements ConnectivityAdapter {
/**
* Returns the current connectivity status.
*
* @returns A promise that always resolves to the online status.
*/
async getStatus(): Promise<ConnectivityStatus> {
return CONNECTIVITY_STATUSES.Online;
}

/**
* Registers a callback for connectivity changes.
*
* This adapter never changes status, so the callback is never invoked.
*
* @param _callback - The callback to register.
*/
onConnectivityChange(_callback: (status: ConnectivityStatus) => void): void {
// no-op
}

/**
* Cleans up any resources held by this adapter.
*
* This adapter holds no resources, so this is a no-op.
*/
destroy(): void {
// no-op
}
}
Comment thread
grypez marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { CONNECTIVITY_STATUSES } from '@metamask/connectivity-controller';
import { Messenger } from '@metamask/messenger';

import { AlwaysOnlineAdapter } from './always-online-adapter';
import { connectivityController } from './connectivity-controller';

describe('connectivityController', () => {
it('reports online status after initialization', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const parent = new Messenger<'Root', any, any>({ namespace: 'Root' });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const messenger = connectivityController.getMessenger(parent as any);
const controller = connectivityController.init({
messenger,
state: undefined,
options: { connectivityAdapter: new AlwaysOnlineAdapter() },
});

expect(controller.state.connectivityStatus).toBe(
CONNECTIVITY_STATUSES.Online,
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
ConnectivityController,
ConnectivityControllerMessenger,
} from '@metamask/connectivity-controller';
import { Messenger } from '@metamask/messenger';

import type {
DefaultActions,
DefaultEvents,
RootMessenger,
} from '../../defaults';
import type { InitializationConfiguration } from '../../types';

export const connectivityController: InitializationConfiguration<
ConnectivityController,
ConnectivityControllerMessenger
> = {
name: 'ConnectivityController',
init: ({ messenger, options }) =>
new ConnectivityController({
messenger,
connectivityAdapter: options.connectivityAdapter,
}),
getMessenger: (parent: RootMessenger<DefaultActions, DefaultEvents>) =>
new Messenger({
namespace: 'ConnectivityController',
parent,
}),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ConnectivityAdapter } from '@metamask/connectivity-controller';

/**
* Per-instance options for the wallet's `ConnectivityController`.
*/
export type ConnectivityControllerInstanceOptions = {
/**
* Platform-specific adapter that observes the device's network connectivity.
* Required because connectivity is inherently platform-specific; node-like
* environments can pass the exported `AlwaysOnlineAdapter`.
*/
connectivityAdapter: ConnectivityAdapter;
};
2 changes: 2 additions & 0 deletions packages/wallet/src/initialization/instances/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { accountsController } from './accounts-controller/accounts-controller';
export { approvalController } from './approval-controller/approval-controller';
export { connectivityController } from './connectivity-controller/connectivity-controller';
export { keyringController } from './keyring-controller';
export { storageService } from './storage-service';
2 changes: 2 additions & 0 deletions packages/wallet/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
RootMessenger,
} from './initialization/defaults';
import type { ApprovalControllerInstanceOptions } from './initialization/instances/approval-controller/types';
import type { ConnectivityControllerInstanceOptions } from './initialization/instances/connectivity-controller/types';
import { GenericEncryptor } from './initialization/instances/keyring-controller';
import { InitializationConfiguration } from './initialization/types';

Expand All @@ -23,6 +24,7 @@ export type WalletOptions = {

export type InstanceSpecificOptions = {
approvalController?: ApprovalControllerInstanceOptions;
connectivityController: ConnectivityControllerInstanceOptions;
keyringController?: {
encryptor?: GenericEncryptor;
keyringBuilders?: KeyringControllerOptions['keyringBuilders'];
Expand Down
2 changes: 2 additions & 0 deletions packages/wallet/tsconfig.build.json
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.

Update tsconfig (without .build) as well please!

Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
"rootDir": "./src"
},
"references": [
{ "path": "../accounts-controller/tsconfig.build.json" },
{ "path": "../approval-controller/tsconfig.build.json" },
{ "path": "../base-controller/tsconfig.build.json" },
{ "path": "../connectivity-controller/tsconfig.build.json" },
{ "path": "../controller-utils/tsconfig.build.json" },
{ "path": "../keyring-controller/tsconfig.build.json" },
{ "path": "../messenger/tsconfig.build.json" },
Expand Down
Loading
Loading