Skip to content

Commit f5e48a5

Browse files
Merge branch 'main' into release/575.0.0
2 parents bf840fb + edd2053 commit f5e48a5

File tree

11 files changed

+321
-132
lines changed

11 files changed

+321
-132
lines changed

packages/account-tree-controller/CHANGELOG.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- Add new group naming for non-HD keyring accounts ([#6679](https://github.com/MetaMask/core/pull/6679))
13+
- Hardware-wallet account groups are now named: "Ledger|Trezor|QR|Lattice|OneKey Account N".
14+
- Private key account groups are now named: "Imported Account N".
15+
- Snap account groups are now named: "Snap Account N".
16+
- Account group names now use natural indexing as a fallback ([#6677](https://github.com/MetaMask/core/pull/6677)), ([#6679](https://github.com/MetaMask/core/pull/6679))
17+
- If a user names his accounts without any indexes, we would just use the number of accounts to compute the next available index.
18+
1019
### Fixed
1120

12-
- Fix group naming for PK/Hardware accounts ([#6677](https://github.com/MetaMask/core/pull/6677))
13-
- Previously, the first PK/Hardware account would start as `Account 2` as opposed to `Account 1` and thus subsequent group names were off as well.
21+
- Fix group naming for non-HD keyring accounts ([#6677](https://github.com/MetaMask/core/pull/6677)), ([#6679](https://github.com/MetaMask/core/pull/6679))
22+
- Previously, the first non-HD keyring account would start as `Account 2` as opposed to `Account 1` and thus subsequent group names were off as well.
1423

1524
## [1.0.0]
1625

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

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ describe('AccountTreeController', () => {
587587
type: AccountGroupType.SingleAccount,
588588
accounts: [MOCK_SNAP_ACCOUNT_2.id],
589589
metadata: {
590-
name: 'Account 1', // Updated: per-wallet numbering (different wallet)
590+
name: 'Snap Account 1', // Updated: per-wallet numbering (different wallet)
591591
pinned: false,
592592
hidden: false,
593593
},
@@ -610,7 +610,7 @@ describe('AccountTreeController', () => {
610610
type: AccountGroupType.SingleAccount,
611611
accounts: [MOCK_HARDWARE_ACCOUNT_1.id],
612612
metadata: {
613-
name: 'Account 1', // Updated: per-wallet numbering (different wallet)
613+
name: 'Ledger Account 1', // Updated: per-wallet numbering (different wallet)
614614
pinned: false,
615615
hidden: false,
616616
},
@@ -652,13 +652,13 @@ describe('AccountTreeController', () => {
652652
},
653653
[expectedKeyringWalletIdGroup]: {
654654
name: {
655-
value: 'Account 1', // Updated: per-wallet numbering (different wallet)
655+
value: 'Ledger Account 1', // Updated: per-wallet numbering (different wallet)
656656
lastUpdatedAt: expect.any(Number),
657657
},
658658
},
659659
[expectedSnapWalletIdGroup]: {
660660
name: {
661-
value: 'Account 1', // Updated: per-wallet numbering (different wallet)
661+
value: 'Snap Account 1', // Updated: per-wallet numbering (different wallet)
662662
lastUpdatedAt: expect.any(Number),
663663
},
664664
},
@@ -2902,9 +2902,9 @@ describe('AccountTreeController', () => {
29022902
// Critical assertion: should have 2 unique names (no duplicates)
29032903
expect(uniqueNames.size).toBe(2);
29042904

2905-
// Due to optimization, names start at wallet.length, so we get "Account 1" and "Account 2"
2906-
expect(allNames).toContain('Account 1');
2907-
expect(allNames).toContain('Account 2');
2905+
// Due to optimization, names start at wallet.length, so we get "Account 3" and "Account 4"
2906+
expect(allNames).toContain('Ledger Account 1');
2907+
expect(allNames).toContain('Ledger Account 2');
29082908

29092909
// Verify they're actually different
29102910
expect(group1.metadata.name).not.toBe(group2.metadata.name);
@@ -4033,4 +4033,125 @@ describe('AccountTreeController', () => {
40334033
).toBe('Conflict Name (2)');
40344034
});
40354035
});
4036+
4037+
describe('naming', () => {
4038+
const mockAccount1 = {
4039+
...MOCK_HARDWARE_ACCOUNT_1,
4040+
id: 'mock-id-1',
4041+
address: '0x123',
4042+
};
4043+
const mockAccount2 = {
4044+
...MOCK_HARDWARE_ACCOUNT_1,
4045+
id: 'mock-id-2',
4046+
address: '0x456',
4047+
};
4048+
const mockAccount3 = {
4049+
...MOCK_HARDWARE_ACCOUNT_1,
4050+
id: 'mock-id-3',
4051+
address: '0x789',
4052+
};
4053+
const mockAccount4 = {
4054+
...MOCK_HARDWARE_ACCOUNT_1,
4055+
id: 'mock-id-4',
4056+
address: '0xabc',
4057+
};
4058+
4059+
const mockWalletId = toAccountWalletId(
4060+
AccountWalletType.Keyring,
4061+
KeyringTypes.ledger,
4062+
);
4063+
4064+
const getAccountGroupFromAccount = (
4065+
controller: AccountTreeController,
4066+
mockAccount: InternalAccount,
4067+
) => {
4068+
const groupId = toAccountGroupId(mockWalletId, mockAccount.address);
4069+
return controller.state.accountTree.wallets[mockWalletId].groups[groupId];
4070+
};
4071+
4072+
it('names non-HD keyrings accounts properly', () => {
4073+
const { controller, messenger } = setup();
4074+
4075+
// Add all 3 accounts.
4076+
[mockAccount1, mockAccount2, mockAccount3].forEach(
4077+
(mockAccount, index) => {
4078+
messenger.publish('AccountsController:accountAdded', mockAccount);
4079+
4080+
const mockGroup = getAccountGroupFromAccount(controller, mockAccount);
4081+
expect(mockGroup).toBeDefined();
4082+
expect(mockGroup.metadata.name).toBe(`Ledger Account ${index + 1}`);
4083+
},
4084+
);
4085+
4086+
// Remove account 2, should still create account 4 afterward.
4087+
messenger.publish('AccountsController:accountRemoved', mockAccount2.id);
4088+
4089+
expect(
4090+
getAccountGroupFromAccount(controller, mockAccount4),
4091+
).toBeUndefined();
4092+
messenger.publish('AccountsController:accountAdded', mockAccount4);
4093+
4094+
const mockGroup4 = getAccountGroupFromAccount(controller, mockAccount4);
4095+
expect(mockGroup4).toBeDefined();
4096+
expect(mockGroup4.metadata.name).toBe('Ledger Account 4');
4097+
4098+
// Now, removing account 3 and 4, should defaults to an index of "2" (since only
4099+
// account 1 remains), thus, re-inserting account 2, should be named "* Account 2".
4100+
messenger.publish('AccountsController:accountRemoved', mockAccount4.id);
4101+
messenger.publish('AccountsController:accountRemoved', mockAccount3.id);
4102+
4103+
expect(
4104+
getAccountGroupFromAccount(controller, mockAccount2),
4105+
).toBeUndefined();
4106+
messenger.publish('AccountsController:accountAdded', mockAccount2);
4107+
4108+
const mockGroup2 = getAccountGroupFromAccount(controller, mockAccount2);
4109+
expect(mockGroup2).toBeDefined();
4110+
expect(mockGroup2.metadata.name).toBe('Ledger Account 2');
4111+
});
4112+
4113+
it('uses natural indexing for pre-existing accounts', () => {
4114+
const { controller } = setup({
4115+
accounts: [mockAccount1, mockAccount2, mockAccount3],
4116+
});
4117+
4118+
controller.init();
4119+
4120+
// After initializing the controller, all accounts should be named appropriately.
4121+
[mockAccount1, mockAccount2, mockAccount3].forEach(
4122+
(mockAccount, index) => {
4123+
const mockGroup = getAccountGroupFromAccount(controller, mockAccount);
4124+
expect(mockGroup).toBeDefined();
4125+
expect(mockGroup.metadata.name).toBe(`Ledger Account ${index + 1}`);
4126+
},
4127+
);
4128+
});
4129+
4130+
it('fallbacks to natural indexing if group names are not using our default name pattern', () => {
4131+
const { controller, messenger } = setup();
4132+
4133+
[mockAccount1, mockAccount2, mockAccount3].forEach((mockAccount) =>
4134+
messenger.publish('AccountsController:accountAdded', mockAccount),
4135+
);
4136+
4137+
const mockGroup1 = getAccountGroupFromAccount(controller, mockAccount1);
4138+
const mockGroup2 = getAccountGroupFromAccount(controller, mockAccount2);
4139+
const mockGroup3 = getAccountGroupFromAccount(controller, mockAccount3);
4140+
expect(mockGroup1).toBeDefined();
4141+
expect(mockGroup2).toBeDefined();
4142+
expect(mockGroup3).toBeDefined();
4143+
4144+
// Rename all accounts to something different than "* Account <index>".
4145+
controller.setAccountGroupName(mockGroup1.id, 'Account A');
4146+
controller.setAccountGroupName(mockGroup2.id, 'The next account');
4147+
controller.setAccountGroupName(mockGroup3.id, 'Best account so far');
4148+
4149+
// Adding a new account should not reset back to "Account 1", but it should
4150+
// use the next natural index, here, "Account 4".
4151+
messenger.publish('AccountsController:accountAdded', mockAccount4);
4152+
const mockGroup4 = getAccountGroupFromAccount(controller, mockAccount4);
4153+
expect(mockGroup4).toBeDefined();
4154+
expect(mockGroup4.metadata.name).toBe('Ledger Account 4');
4155+
});
4156+
});
40364157
});

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

Lines changed: 67 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import {
2-
AccountWalletType,
3-
AccountGroupType,
4-
select,
5-
} from '@metamask/account-api';
1+
import { AccountWalletType, select } from '@metamask/account-api';
62
import type {
73
AccountGroupId,
84
AccountWalletId,
95
AccountSelector,
106
MultichainAccountWalletId,
7+
AccountGroupType,
118
} from '@metamask/account-api';
129
import type { MultichainAccountWalletStatus } from '@metamask/account-api';
1310
import { type AccountId } from '@metamask/accounts-controller';
@@ -259,23 +256,35 @@ export class AccountTreeController extends BaseController<
259256

260257
// Once we have the account tree, we can apply persisted metadata (names + UI states).
261258
let previousSelectedAccountGroupStillExists = false;
262-
for (const wallet of Object.values(wallets)) {
263-
this.#applyAccountWalletMetadata(wallet);
264-
265-
for (const group of Object.values(wallet.groups)) {
266-
if (group.id === previousSelectedAccountGroup) {
267-
previousSelectedAccountGroupStillExists = true;
268-
}
269-
}
270-
}
271-
272259
this.update((state) => {
273260
state.accountTree.wallets = wallets;
274261

275262
// Apply group metadata within the state update
276263
for (const wallet of Object.values(state.accountTree.wallets)) {
264+
this.#applyAccountWalletMetadata(state, wallet.id);
265+
266+
// Used for default group default names (so we use human-indexing here).
267+
let nextNaturalNameIndex = 1;
277268
for (const group of Object.values(wallet.groups)) {
278-
this.#applyAccountGroupMetadata(state, wallet.id, group.id);
269+
this.#applyAccountGroupMetadata(
270+
state,
271+
wallet.id,
272+
group.id,
273+
// FIXME: We should not need this kind of logic if we were not inserting accounts
274+
// 1 by 1. Instead, we should be inserting wallets and groups directly. This would
275+
// allow us to naturally insert a group in the tree AND update its metadata right
276+
// away...
277+
// But here, we have to wait for the entire group to be ready before updating
278+
// its metadata (mainly because we're dealing with single accounts rather than entire
279+
// groups).
280+
// That is why we need this kind of extra parameter.
281+
nextNaturalNameIndex,
282+
);
283+
284+
if (group.id === previousSelectedAccountGroup) {
285+
previousSelectedAccountGroupStillExists = true;
286+
}
287+
nextNaturalNameIndex += 1;
279288
}
280289
}
281290

@@ -342,10 +351,15 @@ export class AccountTreeController extends BaseController<
342351
* first, and then fallbacks to default values (based on the wallet's
343352
* type).
344353
*
345-
* @param wallet Account wallet object to update.
354+
* @param state Controller state to update for persistence.
355+
* @param walletId The wallet ID to update.
346356
*/
347-
#applyAccountWalletMetadata(wallet: AccountWalletObject) {
348-
const persistedMetadata = this.state.accountWalletsMetadata[wallet.id];
357+
#applyAccountWalletMetadata(
358+
state: AccountTreeControllerState,
359+
walletId: AccountWalletId,
360+
) {
361+
const wallet = state.accountTree.wallets[walletId];
362+
const persistedMetadata = state.accountWalletsMetadata[walletId];
349363

350364
// Apply persisted name if available (including empty strings)
351365
if (persistedMetadata?.name !== undefined) {
@@ -402,11 +416,13 @@ export class AccountTreeController extends BaseController<
402416
* @param state Controller state to update for persistence.
403417
* @param walletId The wallet ID containing the group.
404418
* @param groupId The account group ID to update.
419+
* @param nextNaturalNameIndex The next natural name index for this group (only used for default names).
405420
*/
406421
#applyAccountGroupMetadata(
407422
state: AccountTreeControllerState,
408423
walletId: AccountWalletId,
409424
groupId: AccountGroupId,
425+
nextNaturalNameIndex?: number,
410426
) {
411427
const wallet = state.accountTree.wallets[walletId];
412428
const group = wallet.groups[groupId];
@@ -420,15 +436,14 @@ export class AccountTreeController extends BaseController<
420436
// Get the appropriate rule for this wallet type
421437
const rule = this.#getRuleForWallet(wallet);
422438

439+
// Get the prefix for groups of this wallet
440+
const namePrefix = rule.getDefaultAccountGroupPrefix(wallet);
441+
423442
// Skip computed names for now - use default naming with per-wallet logic
424443
// TODO: Implement computed names in a future iteration
425444

426-
// Generate default name and ensure it's unique within the wallet
427-
let proposedName = '';
428-
let proposedNameIndex: number;
429-
430445
// Parse the highest account index being used (similar to accounts-controller)
431-
let highestAccountNameIndex = 0;
446+
let highestNameIndex = 0;
432447
for (const existingGroup of Object.values(
433448
wallet.groups,
434449
) as AccountGroupObject[]) {
@@ -437,50 +452,53 @@ export class AccountTreeController extends BaseController<
437452
continue;
438453
}
439454
// Parse the existing group name to extract the numeric index
440-
// TODO: This regex only matches "Account N" pattern. Hardware wallets (Trezor, Ledger, etc.)
441-
// use different patterns like "Trezor N", "Ledger N" per keyringTypeToName().
442-
// We'll enhance this to handle all keyring types in a future iteration.
443455
const nameMatch = existingGroup.metadata.name.match(/Account (\d+)$/u);
444456
if (nameMatch) {
445457
const nameIndex = parseInt(nameMatch[1], 10);
446-
if (nameIndex > highestAccountNameIndex) {
447-
highestAccountNameIndex = nameIndex;
458+
if (nameIndex > highestNameIndex) {
459+
highestNameIndex = nameIndex;
448460
}
449461
}
450462
}
451463

452-
// For entropy-based multichain groups, start with the actual groupIndex
453-
if (
454-
group.type === AccountGroupType.MultichainAccount &&
455-
group.metadata.entropy
456-
) {
457-
proposedNameIndex = group.metadata.entropy.groupIndex;
458-
} else {
459-
// For other wallet types, start with the number of existing groups
460-
// This gives us the next logical sequential number
461-
proposedNameIndex = Object.keys(wallet.groups).length - 1;
462-
}
463-
464-
// Use the higher of the two: highest parsed index or computed index
465-
proposedNameIndex = Math.min(highestAccountNameIndex, proposedNameIndex);
464+
// We just use the highest known index no matter the wallet type.
465+
//
466+
// For entropy-based wallets (bip44), if a multichain account group with group index 1
467+
// is inserted before another one with group index 0, then the naming will be:
468+
// - "Account 1" (group index 1)
469+
// - "Account 2" (group index 0)
470+
// This naming makes more sense for the end-user.
471+
//
472+
// For other type of wallets, since those wallets can create arbitrary gaps, we still
473+
// rely on the highest know index to avoid back-filling account with "old names".
474+
let proposedNameIndex = Math.max(
475+
// Use + 1 to use the next available index.
476+
highestNameIndex + 1,
477+
// In case all accounts have been renamed differently than the usual "Account <index>"
478+
// pattern, we want to use the next "natural" index, which is just the number of groups
479+
// in that wallet (e.g. ["Account A", "Another Account"], next natural index would be
480+
// "Account 3" in this case).
481+
nextNaturalNameIndex ?? Object.keys(wallet.groups).length,
482+
);
466483

467484
// Find a unique name by checking for conflicts and incrementing if needed
468-
let nameExists: boolean;
485+
let proposedNameExists: boolean;
486+
let proposedName = '';
469487
do {
470-
proposedName = rule.getDefaultAccountGroupName(proposedNameIndex);
488+
proposedName = `${namePrefix} ${proposedNameIndex}`;
471489

472490
// Check if this name already exists in the wallet (excluding current group)
473-
nameExists = !isAccountGroupNameUniqueFromWallet(
491+
proposedNameExists = !isAccountGroupNameUniqueFromWallet(
474492
wallet,
475493
group.id,
476494
proposedName,
477495
);
478496

479497
/* istanbul ignore next */
480-
if (nameExists) {
498+
if (proposedNameExists) {
481499
proposedNameIndex += 1; // Try next number
482500
}
483-
} while (nameExists);
501+
} while (proposedNameExists);
484502

485503
state.accountTree.wallets[walletId].groups[groupId].metadata.name =
486504
proposedName;
@@ -608,7 +626,7 @@ export class AccountTreeController extends BaseController<
608626

609627
const wallet = state.accountTree.wallets[walletId];
610628
if (wallet) {
611-
this.#applyAccountWalletMetadata(wallet);
629+
this.#applyAccountWalletMetadata(state, walletId);
612630
this.#applyAccountGroupMetadata(state, walletId, groupId);
613631
}
614632
}

0 commit comments

Comments
 (0)