@metamask-previews/account-tree-controller 1.0.0-preview-5a701133 → 1.2.0-preview-c20b7569

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +40 -3
  2. package/dist/AccountTreeController.cjs +131 -60
  3. package/dist/AccountTreeController.cjs.map +1 -1
  4. package/dist/AccountTreeController.d.cts +13 -1
  5. package/dist/AccountTreeController.d.cts.map +1 -1
  6. package/dist/AccountTreeController.d.mts +13 -1
  7. package/dist/AccountTreeController.d.mts.map +1 -1
  8. package/dist/AccountTreeController.mjs +133 -62
  9. package/dist/AccountTreeController.mjs.map +1 -1
  10. package/dist/backup-and-sync/syncing/group.cjs +1 -1
  11. package/dist/backup-and-sync/syncing/group.cjs.map +1 -1
  12. package/dist/backup-and-sync/syncing/group.mjs +1 -1
  13. package/dist/backup-and-sync/syncing/group.mjs.map +1 -1
  14. package/dist/backup-and-sync/syncing/legacy.cjs +1 -1
  15. package/dist/backup-and-sync/syncing/legacy.cjs.map +1 -1
  16. package/dist/backup-and-sync/syncing/legacy.mjs +1 -1
  17. package/dist/backup-and-sync/syncing/legacy.mjs.map +1 -1
  18. package/dist/group.cjs +17 -1
  19. package/dist/group.cjs.map +1 -1
  20. package/dist/group.d.cts +7 -0
  21. package/dist/group.d.cts.map +1 -1
  22. package/dist/group.d.mts +7 -0
  23. package/dist/group.d.mts.map +1 -1
  24. package/dist/group.mjs +16 -0
  25. package/dist/group.mjs.map +1 -1
  26. package/dist/rule.cjs +6 -5
  27. package/dist/rule.cjs.map +1 -1
  28. package/dist/rule.d.cts +8 -8
  29. package/dist/rule.d.cts.map +1 -1
  30. package/dist/rule.d.mts +8 -8
  31. package/dist/rule.d.mts.map +1 -1
  32. package/dist/rule.mjs +6 -5
  33. package/dist/rule.mjs.map +1 -1
  34. package/dist/rules/entropy.cjs +2 -2
  35. package/dist/rules/entropy.cjs.map +1 -1
  36. package/dist/rules/entropy.d.cts +1 -1
  37. package/dist/rules/entropy.d.cts.map +1 -1
  38. package/dist/rules/entropy.d.mts +1 -1
  39. package/dist/rules/entropy.d.mts.map +1 -1
  40. package/dist/rules/entropy.mjs +2 -2
  41. package/dist/rules/entropy.mjs.map +1 -1
  42. package/dist/rules/keyring.cjs +44 -3
  43. package/dist/rules/keyring.cjs.map +1 -1
  44. package/dist/rules/keyring.d.cts +8 -1
  45. package/dist/rules/keyring.d.cts.map +1 -1
  46. package/dist/rules/keyring.d.mts +8 -1
  47. package/dist/rules/keyring.d.mts.map +1 -1
  48. package/dist/rules/keyring.mjs +42 -2
  49. package/dist/rules/keyring.mjs.map +1 -1
  50. package/dist/rules/snap.cjs +3 -5
  51. package/dist/rules/snap.cjs.map +1 -1
  52. package/dist/rules/snap.d.cts +1 -3
  53. package/dist/rules/snap.d.cts.map +1 -1
  54. package/dist/rules/snap.d.mts +1 -3
  55. package/dist/rules/snap.d.mts.map +1 -1
  56. package/dist/rules/snap.mjs +3 -5
  57. package/dist/rules/snap.mjs.map +1 -1
  58. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -9,8 +9,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Fixed
11
11
 
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.
12
+ - Prevent `:account{Added,Removed}` to used if `init` has not been called yet ([#6717](https://github.com/MetaMask/core/pull/6717))
13
+ - We now wait for internal accounts to be fully ready (before `init` gets called).
14
+ - This should also enforce account group ordering, since all accounts will be ready to consume right away.
15
+
16
+ ## [1.2.0]
17
+
18
+ ### Added
19
+
20
+ - Add `reinit` method ([#6709](https://github.com/MetaMask/core/pull/6709))
21
+ - This method can be used if we change the entire list of accounts of the `AccountsController` and want to re-initilize the tree with it.
22
+
23
+ ### Changed
24
+
25
+ - Implicitly call `init` before mutating the tree ([#6709](https://github.com/MetaMask/core/pull/6709))
26
+ - This ensure the tree is always using existing accounts before inserting/removing any new accounts if `init` has not been called yet.
27
+
28
+ ### Fixed
29
+
30
+ - Fix use of unknown `group.metadata.name` when checking for group name uniqueness ([#6706](https://github.com/MetaMask/core/pull/6706))
31
+ - Added logic that prevents an account within a group from being out of order ([#6683](https://github.com/MetaMask/core/pull/6683))
32
+
33
+ ## [1.1.0]
34
+
35
+ ### Changed
36
+
37
+ - Set the `setAccountGroupName`'s option `autoHandleConflict` to `true` for all backup & sync operations ([#6697](https://github.com/MetaMask/core/pull/6697))
38
+ - Add new group naming for non-HD keyring accounts ([#6679](https://github.com/MetaMask/core/pull/6679)), ([#6696](https://github.com/MetaMask/core/pull/6696))
39
+ - Hardware-wallet account groups are now named: "Ledger|Trezor|QR|Lattice|OneKey Account N".
40
+ - Private key account groups are now named: "Imported Account N".
41
+ - Snap account groups are now named: "Snap Account N".
42
+ - 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)), ([#6696](https://github.com/MetaMask/core/pull/6696))
43
+ - If a user names his accounts without any indexes, we would just use the number of accounts to compute the next available index.
44
+
45
+ ### Fixed
46
+
47
+ - Fix group naming for non-HD keyring accounts ([#6677](https://github.com/MetaMask/core/pull/6677)), ([#6679](https://github.com/MetaMask/core/pull/6679))
48
+ - 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.
14
49
 
15
50
  ## [1.0.0]
16
51
 
@@ -294,7 +329,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
294
329
  - Initial release ([#5847](https://github.com/MetaMask/core/pull/5847))
295
330
  - Grouping accounts into 3 main categories: Entropy source, Snap ID, keyring types.
296
331
 
297
- [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/account-tree-controller@1.0.0...HEAD
332
+ [Unreleased]: https://github.com/MetaMask/core/compare/@metamask/account-tree-controller@1.2.0...HEAD
333
+ [1.2.0]: https://github.com/MetaMask/core/compare/@metamask/account-tree-controller@1.1.0...@metamask/account-tree-controller@1.2.0
334
+ [1.1.0]: https://github.com/MetaMask/core/compare/@metamask/account-tree-controller@1.0.0...@metamask/account-tree-controller@1.1.0
298
335
  [1.0.0]: https://github.com/MetaMask/core/compare/@metamask/account-tree-controller@0.18.1...@metamask/account-tree-controller@1.0.0
299
336
  [0.18.1]: https://github.com/MetaMask/core/compare/@metamask/account-tree-controller@0.18.0...@metamask/account-tree-controller@0.18.1
300
337
  [0.18.0]: https://github.com/MetaMask/core/compare/@metamask/account-tree-controller@0.17.0...@metamask/account-tree-controller@0.18.0
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _AccountTreeController_instances, _AccountTreeController_accountIdToContext, _AccountTreeController_groupIdToWalletId, _AccountTreeController_backupAndSyncService, _AccountTreeController_rules, _AccountTreeController_trace, _AccountTreeController_backupAndSyncConfig, _AccountTreeController_getEntropyRule, _AccountTreeController_getSnapRule, _AccountTreeController_getKeyringRule, _AccountTreeController_applyAccountWalletMetadata, _AccountTreeController_getRuleForWallet, _AccountTreeController_applyAccountGroupMetadata, _AccountTreeController_handleAccountAdded, _AccountTreeController_handleAccountRemoved, _AccountTreeController_pruneEmptyGroupAndWallet, _AccountTreeController_insert, _AccountTreeController_listAccounts, _AccountTreeController_assertAccountGroupExists, _AccountTreeController_assertAccountWalletExists, _AccountTreeController_assertAccountGroupNameIsUnique, _AccountTreeController_getDefaultSelectedAccountGroup, _AccountTreeController_handleSelectedAccountChange, _AccountTreeController_handleMultichainAccountWalletStatusChange, _AccountTreeController_getAccountGroup, _AccountTreeController_getDefaultAccountFromAccountGroupId, _AccountTreeController_getDefaultAccountGroupId, _AccountTreeController_registerMessageHandlers, _AccountTreeController_createBackupAndSyncContext;
13
+ var _AccountTreeController_instances, _AccountTreeController_accountIdToContext, _AccountTreeController_groupIdToWalletId, _AccountTreeController_backupAndSyncService, _AccountTreeController_rules, _AccountTreeController_trace, _AccountTreeController_backupAndSyncConfig, _AccountTreeController_initialized, _AccountTreeController_getEntropyRule, _AccountTreeController_getSnapRule, _AccountTreeController_getKeyringRule, _AccountTreeController_applyAccountWalletMetadata, _AccountTreeController_getRuleForWallet, _AccountTreeController_applyAccountGroupMetadata, _AccountTreeController_handleAccountAdded, _AccountTreeController_handleAccountRemoved, _AccountTreeController_pruneEmptyGroupAndWallet, _AccountTreeController_insert, _AccountTreeController_listAccounts, _AccountTreeController_assertAccountGroupExists, _AccountTreeController_assertAccountWalletExists, _AccountTreeController_assertAccountGroupNameIsUnique, _AccountTreeController_getDefaultSelectedAccountGroup, _AccountTreeController_handleSelectedAccountChange, _AccountTreeController_handleMultichainAccountWalletStatusChange, _AccountTreeController_getAccountGroup, _AccountTreeController_getDefaultAccountFromAccountGroupId, _AccountTreeController_getDefaultAccountGroupId, _AccountTreeController_registerMessageHandlers, _AccountTreeController_createBackupAndSyncContext;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.AccountTreeController = exports.getDefaultAccountTreeControllerState = exports.controllerName = void 0;
16
16
  const account_api_1 = require("@metamask/account-api");
@@ -103,6 +103,9 @@ class AccountTreeController extends base_controller_1.BaseController {
103
103
  _AccountTreeController_rules.set(this, void 0);
104
104
  _AccountTreeController_trace.set(this, void 0);
105
105
  _AccountTreeController_backupAndSyncConfig.set(this, void 0);
106
+ _AccountTreeController_initialized.set(this, void 0);
107
+ // This will be set to true upon the first `init` call.
108
+ __classPrivateFieldSet(this, _AccountTreeController_initialized, false, "f");
106
109
  // Reverse map to allow fast node access from an account ID.
107
110
  __classPrivateFieldSet(this, _AccountTreeController_accountIdToContext, new Map(), "f");
108
111
  // Reverse map to allow fast wallet node access from a group ID.
@@ -152,6 +155,11 @@ class AccountTreeController extends base_controller_1.BaseController {
152
155
  * state with it.
153
156
  */
154
157
  init() {
158
+ if (__classPrivateFieldGet(this, _AccountTreeController_initialized, "f")) {
159
+ // We prevent re-initilializing the state multiple times. Though, we can use
160
+ // `reinit` to re-init everything from scratch.
161
+ return;
162
+ }
155
163
  const wallets = {};
156
164
  // Clear mappings for fresh rebuild.
157
165
  __classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").clear();
@@ -165,20 +173,28 @@ class AccountTreeController extends base_controller_1.BaseController {
165
173
  }
166
174
  // Once we have the account tree, we can apply persisted metadata (names + UI states).
167
175
  let previousSelectedAccountGroupStillExists = false;
168
- for (const wallet of Object.values(wallets)) {
169
- __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountWalletMetadata).call(this, wallet);
170
- for (const group of Object.values(wallet.groups)) {
171
- if (group.id === previousSelectedAccountGroup) {
172
- previousSelectedAccountGroupStillExists = true;
173
- }
174
- }
175
- }
176
176
  this.update((state) => {
177
177
  state.accountTree.wallets = wallets;
178
178
  // Apply group metadata within the state update
179
179
  for (const wallet of Object.values(state.accountTree.wallets)) {
180
+ __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountWalletMetadata).call(this, state, wallet.id);
181
+ // Used for default group default names (so we use human-indexing here).
182
+ let nextNaturalNameIndex = 1;
180
183
  for (const group of Object.values(wallet.groups)) {
181
- __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountGroupMetadata).call(this, state, wallet.id, group.id);
184
+ __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountGroupMetadata).call(this, state, wallet.id, group.id,
185
+ // FIXME: We should not need this kind of logic if we were not inserting accounts
186
+ // 1 by 1. Instead, we should be inserting wallets and groups directly. This would
187
+ // allow us to naturally insert a group in the tree AND update its metadata right
188
+ // away...
189
+ // But here, we have to wait for the entire group to be ready before updating
190
+ // its metadata (mainly because we're dealing with single accounts rather than entire
191
+ // groups).
192
+ // That is why we need this kind of extra parameter.
193
+ nextNaturalNameIndex);
194
+ if (group.id === previousSelectedAccountGroup) {
195
+ previousSelectedAccountGroupStillExists = true;
196
+ }
197
+ nextNaturalNameIndex += 1;
182
198
  }
183
199
  }
184
200
  if (!previousSelectedAccountGroupStillExists ||
@@ -197,6 +213,17 @@ class AccountTreeController extends base_controller_1.BaseController {
197
213
  this.state.accountTree.selectedAccountGroup) {
198
214
  this.messagingSystem.publish(`${exports.controllerName}:selectedAccountGroupChange`, this.state.accountTree.selectedAccountGroup, previousSelectedAccountGroup);
199
215
  }
216
+ __classPrivateFieldSet(this, _AccountTreeController_initialized, true, "f");
217
+ }
218
+ /**
219
+ * Re-initialize the controller's state.
220
+ *
221
+ * This is done in one single (atomic) `update` block to avoid having a temporary
222
+ * cleared state. Use this when you need to force a full re-init even if already initialized.
223
+ */
224
+ reinit() {
225
+ __classPrivateFieldSet(this, _AccountTreeController_initialized, false, "f");
226
+ this.init();
200
227
  }
201
228
  /**
202
229
  * Gets the account wallet object from its ID.
@@ -468,6 +495,8 @@ class AccountTreeController extends base_controller_1.BaseController {
468
495
  };
469
496
  });
470
497
  __classPrivateFieldGet(this, _AccountTreeController_backupAndSyncService, "f").clearState();
498
+ // So we know we have to call `init` again.
499
+ __classPrivateFieldSet(this, _AccountTreeController_initialized, false, "f");
471
500
  }
472
501
  /**
473
502
  * Bi-directionally syncs the account tree with user storage.
@@ -500,14 +529,15 @@ class AccountTreeController extends base_controller_1.BaseController {
500
529
  }
501
530
  }
502
531
  exports.AccountTreeController = AccountTreeController;
503
- _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeController_groupIdToWalletId = new WeakMap(), _AccountTreeController_backupAndSyncService = new WeakMap(), _AccountTreeController_rules = new WeakMap(), _AccountTreeController_trace = new WeakMap(), _AccountTreeController_backupAndSyncConfig = new WeakMap(), _AccountTreeController_instances = new WeakSet(), _AccountTreeController_getEntropyRule = function _AccountTreeController_getEntropyRule() {
532
+ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeController_groupIdToWalletId = new WeakMap(), _AccountTreeController_backupAndSyncService = new WeakMap(), _AccountTreeController_rules = new WeakMap(), _AccountTreeController_trace = new WeakMap(), _AccountTreeController_backupAndSyncConfig = new WeakMap(), _AccountTreeController_initialized = new WeakMap(), _AccountTreeController_instances = new WeakSet(), _AccountTreeController_getEntropyRule = function _AccountTreeController_getEntropyRule() {
504
533
  return __classPrivateFieldGet(this, _AccountTreeController_rules, "f")[0];
505
534
  }, _AccountTreeController_getSnapRule = function _AccountTreeController_getSnapRule() {
506
535
  return __classPrivateFieldGet(this, _AccountTreeController_rules, "f")[1];
507
536
  }, _AccountTreeController_getKeyringRule = function _AccountTreeController_getKeyringRule() {
508
537
  return __classPrivateFieldGet(this, _AccountTreeController_rules, "f")[2];
509
- }, _AccountTreeController_applyAccountWalletMetadata = function _AccountTreeController_applyAccountWalletMetadata(wallet) {
510
- const persistedMetadata = this.state.accountWalletsMetadata[wallet.id];
538
+ }, _AccountTreeController_applyAccountWalletMetadata = function _AccountTreeController_applyAccountWalletMetadata(state, walletId) {
539
+ const wallet = state.accountTree.wallets[walletId];
540
+ const persistedMetadata = state.accountWalletsMetadata[walletId];
511
541
  // Apply persisted name if available (including empty strings)
512
542
  if (persistedMetadata?.name !== undefined) {
513
543
  wallet.metadata.name = persistedMetadata.name.value;
@@ -536,8 +566,8 @@ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeControlle
536
566
  default:
537
567
  return __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_getKeyringRule).call(this);
538
568
  }
539
- }, _AccountTreeController_applyAccountGroupMetadata = function _AccountTreeController_applyAccountGroupMetadata(state, walletId, groupId) {
540
- var _a, _b;
569
+ }, _AccountTreeController_applyAccountGroupMetadata = function _AccountTreeController_applyAccountGroupMetadata(state, walletId, groupId, nextNaturalNameIndex) {
570
+ var _a;
541
571
  const wallet = state.accountTree.wallets[walletId];
542
572
  const group = wallet.groups[groupId];
543
573
  const persistedGroupMetadata = state.accountGroupsMetadata[group.id];
@@ -549,58 +579,65 @@ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeControlle
549
579
  else if (!group.metadata.name) {
550
580
  // Get the appropriate rule for this wallet type
551
581
  const rule = __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_getRuleForWallet).call(this, wallet);
582
+ // Get the prefix for groups of this wallet
583
+ const namePrefix = rule.getDefaultAccountGroupPrefix(wallet);
552
584
  // Skip computed names for now - use default naming with per-wallet logic
553
585
  // TODO: Implement computed names in a future iteration
554
- // Generate default name and ensure it's unique within the wallet
555
- let proposedName = '';
556
- let proposedNameIndex;
557
586
  // Parse the highest account index being used (similar to accounts-controller)
558
- let highestAccountNameIndex = 0;
559
- for (const existingGroup of Object.values(wallet.groups)) {
587
+ let highestNameIndex = 0;
588
+ for (const { id: otherGroupId } of Object.values(wallet.groups)) {
560
589
  // Skip the current group being processed
561
- if (existingGroup.id === group.id) {
590
+ if (otherGroupId === groupId) {
562
591
  continue;
563
592
  }
593
+ // We always get the name from the persisted map, since `init` will clear the
594
+ // `state.accountTree.wallets`, thus, given empty `group.metadata.name`.
595
+ // NOTE: If the other group has not been named yet, we just use an empty name.
596
+ const otherGroupName = state.accountGroupsMetadata[otherGroupId]?.name?.value ?? '';
564
597
  // Parse the existing group name to extract the numeric index
565
- // TODO: This regex only matches "Account N" pattern. Hardware wallets (Trezor, Ledger, etc.)
566
- // use different patterns like "Trezor N", "Ledger N" per keyringTypeToName().
567
- // We'll enhance this to handle all keyring types in a future iteration.
568
- const nameMatch = existingGroup.metadata.name.match(/Account (\d+)$/u);
598
+ const nameMatch = otherGroupName.match(/account\s+(\d+)$/iu);
569
599
  if (nameMatch) {
570
600
  const nameIndex = parseInt(nameMatch[1], 10);
571
- if (nameIndex > highestAccountNameIndex) {
572
- highestAccountNameIndex = nameIndex;
601
+ if (nameIndex > highestNameIndex) {
602
+ highestNameIndex = nameIndex;
573
603
  }
574
604
  }
575
605
  }
576
- // For entropy-based multichain groups, start with the actual groupIndex
577
- if (group.type === account_api_1.AccountGroupType.MultichainAccount &&
578
- group.metadata.entropy) {
579
- proposedNameIndex = group.metadata.entropy.groupIndex;
580
- }
581
- else {
582
- // For other wallet types, start with the number of existing groups
583
- // This gives us the next logical sequential number
584
- proposedNameIndex = Object.keys(wallet.groups).length - 1;
585
- }
586
- // Use the higher of the two: highest parsed index or computed index
587
- proposedNameIndex = Math.min(highestAccountNameIndex, proposedNameIndex);
606
+ // We just use the highest known index no matter the wallet type.
607
+ //
608
+ // For entropy-based wallets (bip44), if a multichain account group with group index 1
609
+ // is inserted before another one with group index 0, then the naming will be:
610
+ // - "Account 1" (group index 1)
611
+ // - "Account 2" (group index 0)
612
+ // This naming makes more sense for the end-user.
613
+ //
614
+ // For other type of wallets, since those wallets can create arbitrary gaps, we still
615
+ // rely on the highest know index to avoid back-filling account with "old names".
616
+ let proposedNameIndex = Math.max(
617
+ // Use + 1 to use the next available index.
618
+ highestNameIndex + 1,
619
+ // In case all accounts have been renamed differently than the usual "Account <index>"
620
+ // pattern, we want to use the next "natural" index, which is just the number of groups
621
+ // in that wallet (e.g. ["Account A", "Another Account"], next natural index would be
622
+ // "Account 3" in this case).
623
+ nextNaturalNameIndex ?? Object.keys(wallet.groups).length);
588
624
  // Find a unique name by checking for conflicts and incrementing if needed
589
- let nameExists;
625
+ let proposedNameExists;
626
+ let proposedName = '';
590
627
  do {
591
- proposedName = rule.getDefaultAccountGroupName(proposedNameIndex);
628
+ proposedName = `${namePrefix} ${proposedNameIndex}`;
592
629
  // Check if this name already exists in the wallet (excluding current group)
593
- nameExists = !(0, group_1.isAccountGroupNameUniqueFromWallet)(wallet, group.id, proposedName);
630
+ proposedNameExists = !(0, group_1.isAccountGroupNameUniqueFromWallet)(wallet, group.id, proposedName);
594
631
  /* istanbul ignore next */
595
- if (nameExists) {
632
+ if (proposedNameExists) {
596
633
  proposedNameIndex += 1; // Try next number
597
634
  }
598
- } while (nameExists);
635
+ } while (proposedNameExists);
599
636
  state.accountTree.wallets[walletId].groups[groupId].metadata.name =
600
637
  proposedName;
601
638
  // Persist the generated name to ensure consistency
602
- (_a = state.accountGroupsMetadata)[_b = group.id] ?? (_a[_b] = {});
603
- state.accountGroupsMetadata[group.id].name = {
639
+ (_a = state.accountGroupsMetadata)[groupId] ?? (_a[groupId] = {});
640
+ state.accountGroupsMetadata[groupId].name = {
604
641
  value: proposedName,
605
642
  // The `lastUpdatedAt` field is used for backup and sync, when comparing local names
606
643
  // with backed up names. In this case, the generated name should never take precedence
@@ -616,20 +653,36 @@ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeControlle
616
653
  group.metadata.hidden = persistedGroupMetadata.hidden.value;
617
654
  }
618
655
  }, _AccountTreeController_handleAccountAdded = function _AccountTreeController_handleAccountAdded(account) {
619
- this.update((state) => {
620
- __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_insert).call(this, state.accountTree.wallets, account);
621
- const context = __classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").get(account.id);
622
- if (context) {
623
- const { walletId, groupId } = context;
624
- const wallet = state.accountTree.wallets[walletId];
625
- if (wallet) {
626
- __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountWalletMetadata).call(this, wallet);
627
- __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountGroupMetadata).call(this, state, walletId, groupId);
656
+ // We wait for the first `init` to be called to actually build up the tree and
657
+ // mutate it. We expect the caller to first update the `AccountsController` state
658
+ // to force the migration of accounts, and then call `init`.
659
+ if (!__classPrivateFieldGet(this, _AccountTreeController_initialized, "f")) {
660
+ return;
661
+ }
662
+ // Check if this account got already added by `#initAtLeastOnce`, if not, then we
663
+ // can proceed.
664
+ if (!__classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").has(account.id)) {
665
+ this.update((state) => {
666
+ __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_insert).call(this, state.accountTree.wallets, account);
667
+ const context = __classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").get(account.id);
668
+ if (context) {
669
+ const { walletId, groupId } = context;
670
+ const wallet = state.accountTree.wallets[walletId];
671
+ if (wallet) {
672
+ __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountWalletMetadata).call(this, state, walletId);
673
+ __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountGroupMetadata).call(this, state, walletId, groupId);
674
+ }
628
675
  }
629
- }
630
- });
631
- this.messagingSystem.publish(`${exports.controllerName}:accountTreeChange`, this.state.accountTree);
676
+ });
677
+ this.messagingSystem.publish(`${exports.controllerName}:accountTreeChange`, this.state.accountTree);
678
+ }
632
679
  }, _AccountTreeController_handleAccountRemoved = function _AccountTreeController_handleAccountRemoved(accountId) {
680
+ // We wait for the first `init` to be called to actually build up the tree and
681
+ // mutate it. We expect the caller to first update the `AccountsController` state
682
+ // to force the migration of accounts, and then call `init`.
683
+ if (!__classPrivateFieldGet(this, _AccountTreeController_initialized, "f")) {
684
+ return;
685
+ }
633
686
  const context = __classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").get(accountId);
634
687
  if (context) {
635
688
  const { walletId, groupId } = context;
@@ -701,11 +754,13 @@ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeControlle
701
754
  }
702
755
  const groupId = result.group.id;
703
756
  let group = wallet.groups[groupId];
757
+ const { type, id } = account;
758
+ const sortOrder = group_1.ACCOUNT_TYPE_TO_SORT_ORDER[type];
704
759
  if (!group) {
705
760
  wallet.groups[groupId] = {
706
761
  ...result.group,
707
762
  // Type-wise, we are guaranteed to always have at least 1 account.
708
- accounts: [account.id],
763
+ accounts: [id],
709
764
  metadata: {
710
765
  name: '',
711
766
  ...{ pinned: false, hidden: false },
@@ -723,12 +778,28 @@ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeControlle
723
778
  }
724
779
  }
725
780
  else {
726
- group.accounts.push(account.id);
781
+ group.accounts.push(id);
782
+ // We need to do this at every insertion because race conditions can happen
783
+ // during the account creation process where one provider completes before the other.
784
+ // The discovery process in the service can also lead to some accounts being created "out of order".
785
+ const { accounts } = group;
786
+ accounts.sort(
787
+ /* istanbul ignore next: Comparator branch execution (a===id vs b===id)
788
+ * and return attribution vary across engines; final ordering is covered
789
+ * by behavior tests. Ignoring the entire comparator avoids flaky line
790
+ * coverage without reducing scenario coverage.
791
+ */
792
+ (a, b) => {
793
+ const aSortOrder = a === id ? sortOrder : __classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").get(a)?.sortOrder;
794
+ const bSortOrder = b === id ? sortOrder : __classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").get(b)?.sortOrder;
795
+ return ((aSortOrder ?? group_1.MAX_SORT_ORDER) - (bSortOrder ?? group_1.MAX_SORT_ORDER));
796
+ });
727
797
  }
728
798
  // Update the reverse mapping for this account.
729
799
  __classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").set(account.id, {
730
800
  walletId: wallet.id,
731
801
  groupId: group.id,
802
+ sortOrder,
732
803
  });
733
804
  }, _AccountTreeController_listAccounts = function _AccountTreeController_listAccounts() {
734
805
  return this.messagingSystem.call('AccountsController:listMultichainAccounts');