Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
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]

### Added

- Add support for `SnapKeyring` v2 accounts ([#8513](https://github.com/MetaMask/core/pull/8513))

### Changed

- Bump `@metamask/messenger` from `^1.0.0` to `^1.1.1` ([#8364](https://github.com/MetaMask/core/pull/8364), [#8373](https://github.com/MetaMask/core/pull/8373))
Expand Down
4 changes: 2 additions & 2 deletions packages/accounts-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"build:docs": "typedoc",
"changelog:update": "../../scripts/update-changelog.sh @metamask/accounts-controller",
"changelog:validate": "../../scripts/validate-changelog.sh @metamask/accounts-controller",
"messenger-action-types:check": "tsx ../../packages/messenger-cli/src/cli.ts --check",
"messenger-action-types:generate": "tsx ../../packages/messenger-cli/src/cli.ts --generate",
"messenger-action-types:check": "tsx ../../packages/messenger-cli/src/cli.ts --formatter oxfmt --check",
"messenger-action-types:generate": "tsx ../../packages/messenger-cli/src/cli.ts --formatter oxfmt --generate",
"since-latest-release": "../../scripts/since-latest-release.sh",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --reporters=jest-silent-reporter",
"test:clean": "NODE_OPTIONS=--experimental-vm-modules jest --clearCache",
Expand Down
291 changes: 291 additions & 0 deletions packages/accounts-controller/src/AccountsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
InfuraNetworkType,
toChecksumHexAddress,
} from '@metamask/controller-utils';
import { SnapKeyring as SnapKeyringV2 } from '@metamask/eth-snap-keyring/v2';
import type {
AccountAssetListUpdatedEventPayload,
AccountBalancesUpdatedEventPayload,
Expand All @@ -16,6 +17,7 @@ import {
EthScope,
KeyringAccountEntropyTypeOption,
} from '@metamask/keyring-api';
import { KeyringType } from '@metamask/keyring-api/v2';
import {
KeyringControllerState,
KeyringTypes,
Expand Down Expand Up @@ -1068,6 +1070,182 @@ describe('AccountsController', () => {
]);
});

it('add Snap v2 accounts', () => {
const mockSnapV2Address = '0xabcdef1234567890abcdef1234567890abcdef12';
const mockSnapV2SnapId = 'mock-snap-v2-id';
const mockSnapV2AccountId = 'mock-snap-v2-account-id';

const mockSnapV2KeyringAccount = {
id: mockSnapV2AccountId,
address: mockSnapV2Address,
options: {},
methods: [...ETH_EOA_METHODS],
type: EthAccountType.Eoa,
scopes: [EthScope.Eoa],
};

const mockKeyringV2Instance = Object.assign(
Object.create(SnapKeyringV2.prototype),
{
snapId: mockSnapV2SnapId,
lookupByAddress: jest
.fn()
.mockReturnValue(mockSnapV2KeyringAccount),
},
);

const messenger = buildMessenger();
messenger.registerActionHandler(
'KeyringController:getKeyringsByType',
mockGetKeyringByType.mockReturnValue([mockKeyringV2Instance]),
);

const mockNewKeyringState = {
isUnlocked: true,
keyrings: [
{
type: KeyringType.Snap,
accounts: [mockSnapV2Address],
metadata: {
id: 'mock-keyring-v2-id',
name: 'mock-keyring-v2-name',
},
},
],
};

const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {},
selectedAccount: '',
},
accountIdByAddress: {},
},
messenger,
});

messenger.publish(
'KeyringController:stateChange',
mockNewKeyringState,
[],
);

const accounts = accountsController.listMultichainAccounts();

expect(accounts).toHaveLength(1);
expect(accounts[0]).toMatchObject({
id: mockSnapV2AccountId,
address: mockSnapV2Address,
metadata: {
keyring: { type: KeyringType.Snap },
snap: {
id: mockSnapV2SnapId,
name: '',
enabled: true,
},
importTime: expect.any(Number),
},
});
});

it('handles the event when a Snap v2 deleted the account before it was added', () => {
const mockSnapV2Address = '0xabcdef1234567890abcdef1234567890abcdef12';

const mockKeyringV2Instance = Object.assign(
Object.create(SnapKeyringV2.prototype),
{
snapId: 'mock-snap-v2-id',
lookupByAddress: jest.fn().mockReturnValue(undefined),
},
);

const messenger = buildMessenger();
messenger.registerActionHandler(
'KeyringController:getKeyringsByType',
mockGetKeyringByType.mockReturnValue([mockKeyringV2Instance]),
);

const mockNewKeyringState = {
isUnlocked: true,
keyrings: [
{
type: KeyringType.Snap,
accounts: [mockSnapV2Address],
metadata: {
id: 'mock-keyring-v2-id',
name: 'mock-keyring-v2-name',
},
},
],
};

const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {},
selectedAccount: '',
},
accountIdByAddress: {},
},
messenger,
});

messenger.publish(
'KeyringController:stateChange',
mockNewKeyringState,
[],
);

expect(accountsController.listMultichainAccounts()).toStrictEqual([]);
});

it('handles when no SnapKeyringV2 instance matches for a Snap v2 keyring type', () => {
const mockSnapV2Address = '0xabcdef1234567890abcdef1234567890abcdef12';

const messenger = buildMessenger();
messenger.registerActionHandler(
'KeyringController:getKeyringsByType',
mockGetKeyringByType.mockReturnValue([
// Plain object — does NOT pass instanceof SnapKeyringV2
{ lookupByAddress: jest.fn() },
]),
);

const mockNewKeyringState = {
isUnlocked: true,
keyrings: [
{
type: KeyringType.Snap,
accounts: [mockSnapV2Address],
metadata: {
id: 'mock-keyring-v2-id',
name: 'mock-keyring-v2-name',
},
},
],
};

const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {},
selectedAccount: '',
},
accountIdByAddress: {},
},
messenger,
});

messenger.publish(
'KeyringController:stateChange',
mockNewKeyringState,
[],
);

expect(accountsController.listMultichainAccounts()).toStrictEqual([]);
});

it('increment the default account number when adding an account', async () => {
const messenger = buildMessenger();

Expand Down Expand Up @@ -3295,6 +3473,119 @@ describe('AccountsController', () => {
expect(mockGetKeyringByType).toHaveBeenCalledTimes(1);
});

it('update accounts with Snap v2 accounts', async () => {
const mockSnapV2Address = '0xabcdef1234567890abcdef1234567890abcdef12';
const mockSnapV2SnapId = 'mock-snap-v2-id';
const mockSnapV2AccountId = 'mock-snap-v2-account-id';

const mockSnapV2KeyringAccount = {
id: mockSnapV2AccountId,
address: mockSnapV2Address,
options: {},
methods: [...ETH_EOA_METHODS],
type: EthAccountType.Eoa,
scopes: [EthScope.Eoa],
};

const mockKeyringV2Instance = Object.assign(
Object.create(SnapKeyringV2.prototype),
{
snapId: mockSnapV2SnapId,
lookupByAddress: jest.fn().mockReturnValue(mockSnapV2KeyringAccount),
},
);

const messenger = buildMessenger();
messenger.registerActionHandler(
'KeyringController:getState',
mockGetState.mockReturnValue({
keyrings: [
{
type: KeyringType.Snap,
accounts: [mockSnapV2Address],
metadata: {
id: 'mock-keyring-v2-id',
name: 'mock-keyring-v2-name',
},
},
],
}),
);
messenger.registerActionHandler(
'KeyringController:getKeyringsByType',
mockGetKeyringByType.mockReturnValue([mockKeyringV2Instance]),
);

const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {},
selectedAccount: '',
},
accountIdByAddress: {},
},
messenger,
});

await accountsController.updateAccounts();

const accounts = accountsController.listMultichainAccounts();
expect(accounts).toHaveLength(1);
expect(accounts[0]).toMatchObject({
id: mockSnapV2AccountId,
address: mockSnapV2Address,
metadata: {
name: 'Snap Account 1',
keyring: { type: KeyringType.Snap },
snap: {
id: mockSnapV2SnapId,
name: '',
enabled: true,
},
},
});
});

it('skips Snap v2 account if no SnapKeyringV2 instance is found', async () => {
const mockSnapV2Address = '0xabcdef1234567890abcdef1234567890abcdef12';

const messenger = buildMessenger();
messenger.registerActionHandler(
'KeyringController:getState',
mockGetState.mockReturnValue({
keyrings: [
{
type: KeyringType.Snap,
accounts: [mockSnapV2Address],
metadata: {
id: 'mock-keyring-v2-id',
name: 'mock-keyring-v2-name',
},
},
],
}),
);
messenger.registerActionHandler(
'KeyringController:getKeyringsByType',
mockGetKeyringByType.mockReturnValue([]),
);

const { accountsController } = setupAccountsController({
initialState: {
internalAccounts: {
accounts: {},
selectedAccount: '',
},
accountIdByAddress: {},
},
messenger,
});

await accountsController.updateAccounts();

expect(accountsController.listMultichainAccounts()).toStrictEqual([]);
});

it.todo(
'does not re-fire a accountChanged event if the account is still the same',
);
Expand Down
Loading
Loading