Skip to content

Commit 3de25ba

Browse files
authored
fix(account-tree-controller): fix duplicate names for out-of-order accounts (#6706)
## Explanation We were not re-using the persisted group name when checking for the next available name during group metadata updates. Meaning that if somehow, we were receiving a `:accountAdded` before `init` was called, then the groups metadata would be empty, leading to some duplicate names. I spotted this issue with mobile E2E and our fixture system. ## References E2E were failing on this PR, because of this bug: - MetaMask/metamask-mobile#20255 ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [ ] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs), highlighting breaking changes as necessary - [ ] I've prepared draft pull requests for clients and consumer packages to resolve any breaking changes
1 parent 93c3e12 commit 3de25ba

File tree

3 files changed

+58
-6
lines changed

3 files changed

+58
-6
lines changed

packages/account-tree-controller/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Fixed
1111

12+
- Fix use of unknown `group.metadata.name` when checking for group name uniqueness ([#6706](https://github.com/MetaMask/core/pull/6706))
1213
- Added logic that prevents an account within a group from being out of order ([#6683](https://github.com/MetaMask/core/pull/6683))
1314

1415
## [1.1.0]

packages/account-tree-controller/src/AccountTreeController.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4154,6 +4154,51 @@ describe('AccountTreeController', () => {
41544154
return controller.state.accountTree.wallets[mockWalletId].groups[groupId];
41554155
};
41564156

4157+
it('names all accounts properly even if they are not ordered naturally', () => {
4158+
const mockHdAccount1 = MOCK_HD_ACCOUNT_1;
4159+
const mockHdAccount2 = {
4160+
...MOCK_HD_ACCOUNT_1,
4161+
id: 'mock-id-2',
4162+
address: '0x456',
4163+
options: {
4164+
entropy: {
4165+
...MOCK_HD_ACCOUNT_1.options.entropy,
4166+
groupIndex: 1,
4167+
},
4168+
},
4169+
};
4170+
4171+
const { controller, mocks } = setup({
4172+
// We start with 1 account (index 0).
4173+
accounts: [mockHdAccount1],
4174+
keyrings: [MOCK_HD_KEYRING_1],
4175+
});
4176+
4177+
controller.init();
4178+
4179+
// Then, we insert a second account (index 1), but we re-order it so it appears
4180+
// before the first account (index 0).
4181+
mocks.AccountsController.accounts = [mockHdAccount2, mockHdAccount1];
4182+
4183+
// Re-init the controller should still give proper naming.
4184+
controller.init();
4185+
4186+
[mockHdAccount1, mockHdAccount2].forEach((mockAccount, index) => {
4187+
const walletId = toMultichainAccountWalletId(
4188+
mockAccount.options.entropy.id,
4189+
);
4190+
const groupId = toMultichainAccountGroupId(
4191+
walletId,
4192+
mockAccount.options.entropy.groupIndex,
4193+
);
4194+
4195+
const mockGroup =
4196+
controller.state.accountTree.wallets[walletId].groups[groupId];
4197+
expect(mockGroup).toBeDefined();
4198+
expect(mockGroup.metadata.name).toBe(`Account ${index + 1}`);
4199+
});
4200+
});
4201+
41574202
it('names non-HD keyrings accounts properly', () => {
41584203
const { controller, messenger } = setup();
41594204

packages/account-tree-controller/src/AccountTreeController.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -451,16 +451,22 @@ export class AccountTreeController extends BaseController<
451451

452452
// Parse the highest account index being used (similar to accounts-controller)
453453
let highestNameIndex = 0;
454-
for (const existingGroup of Object.values(
454+
for (const { id: otherGroupId } of Object.values(
455455
wallet.groups,
456456
) as AccountGroupObject[]) {
457457
// Skip the current group being processed
458-
if (existingGroup.id === group.id) {
458+
if (otherGroupId === groupId) {
459459
continue;
460460
}
461+
462+
// We always get the name from the persisted map, since `init` will clear the
463+
// `state.accountTree.wallets`, thus, given empty `group.metadata.name`.
464+
// NOTE: If the other group has not been named yet, we just use an empty name.
465+
const otherGroupName =
466+
state.accountGroupsMetadata[otherGroupId]?.name?.value ?? '';
467+
461468
// Parse the existing group name to extract the numeric index
462-
const nameMatch =
463-
existingGroup.metadata.name.match(/account\s+(\d+)$/iu);
469+
const nameMatch = otherGroupName.match(/account\s+(\d+)$/iu);
464470
if (nameMatch) {
465471
const nameIndex = parseInt(nameMatch[1], 10);
466472
if (nameIndex > highestNameIndex) {
@@ -512,8 +518,8 @@ export class AccountTreeController extends BaseController<
512518
proposedName;
513519

514520
// Persist the generated name to ensure consistency
515-
state.accountGroupsMetadata[group.id] ??= {};
516-
state.accountGroupsMetadata[group.id].name = {
521+
state.accountGroupsMetadata[groupId] ??= {};
522+
state.accountGroupsMetadata[groupId].name = {
517523
value: proposedName,
518524
// The `lastUpdatedAt` field is used for backup and sync, when comparing local names
519525
// with backed up names. In this case, the generated name should never take precedence

0 commit comments

Comments
 (0)