Skip to content
Merged
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
2 changes: 1 addition & 1 deletion eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"packages/account-tree-controller/src/AccountTreeController.ts": {
"@typescript-eslint/explicit-function-return-type": {
"count": 8
"count": 7
},
"@typescript-eslint/prefer-nullish-coalescing": {
"count": 1
Expand Down
5 changes: 5 additions & 0 deletions packages/account-tree-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `AccountTreeController:accountGroup{Created,Updated,Removed}` events ([#8766](https://github.com/MetaMask/core/pull/8766))
- None of these events fire during `init`/`reinit`, consumers should bootstrap from `:getState` or `:accountTreeChange`.

### Changed

- Bump `@metamask/accounts-controller` from `^38.0.0` to `^38.1.0` ([#8755](https://github.com/MetaMask/core/pull/8755))
Expand Down
308 changes: 308 additions & 0 deletions packages/account-tree-controller/src/AccountTreeController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4740,6 +4740,314 @@ describe('AccountTreeController', () => {

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

it('does NOT emit accountGroupCreated or accountGroupUpdated during init', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1],
});

const createdListener = jest.fn();
const updatedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupCreated',
createdListener,
);
messenger.subscribe(
'AccountTreeController:accountGroupUpdated',
updatedListener,
);

controller.init();

expect(createdListener).not.toHaveBeenCalled();
expect(updatedListener).not.toHaveBeenCalled();
});

it('emits accountGroupCreated when a new group is added post-init', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2],
});

const createdListener = jest.fn();
const updatedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupCreated',
createdListener,
);
messenger.subscribe(
'AccountTreeController:accountGroupUpdated',
updatedListener,
);

controller.init();
jest.clearAllMocks();

messenger.publish('AccountsController:accountsAdded', [
{ ...MOCK_HD_ACCOUNT_2 },
]);

const newWalletId = toMultichainAccountWalletId(
MOCK_HD_KEYRING_2.metadata.id,
);
const newGroupId = toMultichainAccountGroupId(
newWalletId,
MOCK_HD_ACCOUNT_2.options.entropy.groupIndex,
);
const expectedGroup =
controller.state.accountTree.wallets[newWalletId].groups[newGroupId];

expect(createdListener).toHaveBeenCalledTimes(1);
expect(createdListener).toHaveBeenCalledWith(expectedGroup);
expect(updatedListener).not.toHaveBeenCalled();
});

it('emits accountGroupUpdated when an account is added to an existing group', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1],
});

const createdListener = jest.fn();
const updatedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupCreated',
createdListener,
);
messenger.subscribe(
'AccountTreeController:accountGroupUpdated',
updatedListener,
);

controller.init();
jest.clearAllMocks();

messenger.publish('AccountsController:accountsAdded', [
{ ...MOCK_TRX_ACCOUNT_1 },
]);

const walletId = toMultichainAccountWalletId(
MOCK_HD_KEYRING_1.metadata.id,
);
const groupId = toMultichainAccountGroupId(
walletId,
MOCK_HD_ACCOUNT_1.options.entropy.groupIndex,
);
const expectedGroup =
controller.state.accountTree.wallets[walletId].groups[groupId];

expect(updatedListener).toHaveBeenCalledTimes(1);
expect(updatedListener).toHaveBeenCalledWith(expectedGroup);
expect(createdListener).not.toHaveBeenCalled();
});

it('emits accountGroupUpdated when an account is removed but the group remains', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1, MOCK_TRX_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1],
});

const updatedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupUpdated',
updatedListener,
);

controller.init();
jest.clearAllMocks();

messenger.publish('AccountsController:accountsRemoved', [
MOCK_TRX_ACCOUNT_1.id,
]);

const walletId = toMultichainAccountWalletId(
MOCK_HD_KEYRING_1.metadata.id,
);
const groupId = toMultichainAccountGroupId(
walletId,
MOCK_HD_ACCOUNT_1.options.entropy.groupIndex,
);
const expectedGroup =
controller.state.accountTree.wallets[walletId].groups[groupId];

expect(updatedListener).toHaveBeenCalledTimes(1);
expect(updatedListener).toHaveBeenCalledWith(expectedGroup);
});

it('does NOT emit accountGroupUpdated when a removed account causes the group to be pruned', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1, MOCK_SNAP_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2],
});

const updatedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupUpdated',
updatedListener,
);

controller.init();
jest.clearAllMocks();

messenger.publish('AccountsController:accountsRemoved', [
MOCK_SNAP_ACCOUNT_1.id,
]);

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

it('emits accountGroupRemoved when the last account of a group is removed', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1, MOCK_SNAP_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1, MOCK_HD_KEYRING_2],
});

const removedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupRemoved',
removedListener,
);

controller.init();
jest.clearAllMocks();

const removedWalletId = toMultichainAccountWalletId(
MOCK_HD_KEYRING_2.metadata.id,
);
const removedGroupId = toMultichainAccountGroupId(
removedWalletId,
MOCK_SNAP_ACCOUNT_1.options.entropy.groupIndex,
);

messenger.publish('AccountsController:accountsRemoved', [
MOCK_SNAP_ACCOUNT_1.id,
]);

expect(removedListener).toHaveBeenCalledTimes(1);
expect(removedListener).toHaveBeenCalledWith(removedGroupId);
});

it('does NOT emit accountGroupRemoved when the group still has accounts', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1, MOCK_TRX_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1],
});

const removedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupRemoved',
removedListener,
);

controller.init();
jest.clearAllMocks();

messenger.publish('AccountsController:accountsRemoved', [
MOCK_TRX_ACCOUNT_1.id,
]);

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

it('emits accountGroupUpdated when setAccountGroupName is called', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1],
});

const updatedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupUpdated',
updatedListener,
);

controller.init();
jest.clearAllMocks();

const walletId = toMultichainAccountWalletId(
MOCK_HD_KEYRING_1.metadata.id,
);
const groupId = toMultichainAccountGroupId(
walletId,
MOCK_HD_ACCOUNT_1.options.entropy.groupIndex,
);

controller.setAccountGroupName(groupId, 'Renamed Group');

const expectedGroup =
controller.state.accountTree.wallets[walletId].groups[groupId];

expect(updatedListener).toHaveBeenCalledTimes(1);
expect(updatedListener).toHaveBeenCalledWith(expectedGroup);
expect(expectedGroup.metadata.name).toBe('Renamed Group');
});

it('emits accountGroupUpdated when setAccountGroupPinned is called', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1],
});

const updatedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupUpdated',
updatedListener,
);

controller.init();
jest.clearAllMocks();

const walletId = toMultichainAccountWalletId(
MOCK_HD_KEYRING_1.metadata.id,
);
const groupId = toMultichainAccountGroupId(
walletId,
MOCK_HD_ACCOUNT_1.options.entropy.groupIndex,
);

controller.setAccountGroupPinned(groupId, true);

const expectedGroup =
controller.state.accountTree.wallets[walletId].groups[groupId];

expect(updatedListener).toHaveBeenCalledTimes(1);
expect(updatedListener).toHaveBeenCalledWith(expectedGroup);
expect(expectedGroup.metadata.pinned).toBe(true);
});

it('emits accountGroupUpdated when setAccountGroupHidden is called', () => {
const { controller, messenger } = setup({
accounts: [MOCK_HD_ACCOUNT_1],
keyrings: [MOCK_HD_KEYRING_1],
});

const updatedListener = jest.fn();
messenger.subscribe(
'AccountTreeController:accountGroupUpdated',
updatedListener,
);

controller.init();
jest.clearAllMocks();

const walletId = toMultichainAccountWalletId(
MOCK_HD_KEYRING_1.metadata.id,
);
const groupId = toMultichainAccountGroupId(
walletId,
MOCK_HD_ACCOUNT_1.options.entropy.groupIndex,
);

controller.setAccountGroupHidden(groupId, true);

const expectedGroup =
controller.state.accountTree.wallets[walletId].groups[groupId];

expect(updatedListener).toHaveBeenCalledTimes(1);
expect(updatedListener).toHaveBeenCalledWith(expectedGroup);
expect(expectedGroup.metadata.hidden).toBe(true);
});
});

describe('syncWithUserStorage', () => {
Expand Down
Loading
Loading