@metamask-previews/account-tree-controller 1.3.0-preview-a5441fae → 1.3.0-preview-683bbcb0

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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  ### Changed
11
11
 
12
+ - Re-introduce computed names for account groups ([#6758](https://github.com/MetaMask/core/pull/6758))
13
+ - Those names are computed using the old internal account names, allowing to automatically migrate them.
14
+ - We only consider EVM account names.
15
+ - This automatically handles conflicting names, similarly to backup & sync (adding a suffix ` (n)` in case of conflicts.
12
16
  - Bump `@metamask/utils` from `^11.8.0` to `^11.8.1` ([#6708](https://github.com/MetaMask/core/pull/6708))
13
17
 
14
18
  ## [1.3.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_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_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_getComputedAccountGroupName, _AccountTreeController_getDefaultAccountGroupName, _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");
@@ -137,6 +137,9 @@ class AccountTreeController extends base_controller_1.BaseController {
137
137
  this.messagingSystem.subscribe('AccountsController:accountRemoved', (accountId) => {
138
138
  __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_handleAccountRemoved).call(this, accountId);
139
139
  });
140
+ this.messagingSystem.subscribe('AccountsController:selectedAccountChange', (account) => {
141
+ __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_handleSelectedAccountChange).call(this, account);
142
+ });
140
143
  this.messagingSystem.subscribe('UserStorageController:stateChange', (userStorageControllerState) => {
141
144
  __classPrivateFieldGet(this, _AccountTreeController_backupAndSyncService, "f").handleUserStorageStateChange(userStorageControllerState);
142
145
  });
@@ -192,16 +195,22 @@ class AccountTreeController extends base_controller_1.BaseController {
192
195
  // Used for default group default names (so we use human-indexing here).
193
196
  let nextNaturalNameIndex = 1;
194
197
  for (const group of Object.values(wallet.groups)) {
195
- __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountGroupMetadata).call(this, state, wallet.id, group.id,
196
- // FIXME: We should not need this kind of logic if we were not inserting accounts
197
- // 1 by 1. Instead, we should be inserting wallets and groups directly. This would
198
- // allow us to naturally insert a group in the tree AND update its metadata right
199
- // away...
200
- // But here, we have to wait for the entire group to be ready before updating
201
- // its metadata (mainly because we're dealing with single accounts rather than entire
202
- // groups).
203
- // That is why we need this kind of extra parameter.
204
- nextNaturalNameIndex);
198
+ __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_applyAccountGroupMetadata).call(this, state, wallet.id, group.id, {
199
+ // We allow computed name when initializing the tree.
200
+ // This will automatically handle account name migration for the very first init of the
201
+ // tree. Once groups are created, their name will be persisted, thus, taking precedence
202
+ // over the computed names (even if we re-init).
203
+ allowComputedName: true,
204
+ // FIXME: We should not need this kind of logic if we were not inserting accounts
205
+ // 1 by 1. Instead, we should be inserting wallets and groups directly. This would
206
+ // allow us to naturally insert a group in the tree AND update its metadata right
207
+ // away...
208
+ // But here, we have to wait for the entire group to be ready before updating
209
+ // its metadata (mainly because we're dealing with single accounts rather than entire
210
+ // groups).
211
+ // That is why we need this kind of extra parameter.
212
+ nextNaturalNameIndex,
213
+ });
205
214
  if (group.id === previousSelectedAccountGroup) {
206
215
  previousSelectedAccountGroupStillExists = true;
207
216
  }
@@ -584,7 +593,82 @@ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeControlle
584
593
  default:
585
594
  return __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_getKeyringRule).call(this);
586
595
  }
587
- }, _AccountTreeController_applyAccountGroupMetadata = function _AccountTreeController_applyAccountGroupMetadata(state, walletId, groupId, nextNaturalNameIndex) {
596
+ }, _AccountTreeController_getComputedAccountGroupName = function _AccountTreeController_getComputedAccountGroupName(wallet, group) {
597
+ let proposedName = ''; // Empty means there's no computed name for this group.
598
+ for (const id of group.accounts) {
599
+ const account = this.messagingSystem.call('AccountsController:getAccount', id);
600
+ if (!account) {
601
+ continue;
602
+ }
603
+ // We only consider EVM account types for computed names.
604
+ if ((0, keyring_api_1.isEvmAccountType)(account.type) && account.metadata.name.length) {
605
+ proposedName = account.metadata.name;
606
+ break;
607
+ }
608
+ }
609
+ // If this name already exists for whatever reason, we rename it to resolve this conflict.
610
+ if (proposedName.length &&
611
+ !(0, group_1.isAccountGroupNameUniqueFromWallet)(wallet, group.id, proposedName)) {
612
+ proposedName = this.resolveNameConflict(wallet, group.id, proposedName);
613
+ }
614
+ return proposedName;
615
+ }, _AccountTreeController_getDefaultAccountGroupName = function _AccountTreeController_getDefaultAccountGroupName(state, wallet, group, nextNaturalNameIndex) {
616
+ // Get the appropriate rule for this wallet type
617
+ const rule = __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_getRuleForWallet).call(this, wallet);
618
+ // Get the prefix for groups of this wallet
619
+ const namePrefix = rule.getDefaultAccountGroupPrefix(wallet);
620
+ // Parse the highest account index being used (similar to accounts-controller)
621
+ let highestNameIndex = 0;
622
+ for (const { id: otherGroupId } of Object.values(wallet.groups)) {
623
+ // Skip the current group being processed
624
+ if (otherGroupId === group.id) {
625
+ continue;
626
+ }
627
+ // We always get the name from the persisted map, since `init` will clear the
628
+ // `state.accountTree.wallets`, thus, given empty `group.metadata.name`.
629
+ // NOTE: If the other group has not been named yet, we just use an empty name.
630
+ const otherGroupName = state.accountGroupsMetadata[otherGroupId]?.name?.value ?? '';
631
+ // Parse the existing group name to extract the numeric index
632
+ const nameMatch = otherGroupName.match(/account\s+(\d+)$/iu);
633
+ if (nameMatch) {
634
+ const nameIndex = parseInt(nameMatch[1], 10);
635
+ if (nameIndex > highestNameIndex) {
636
+ highestNameIndex = nameIndex;
637
+ }
638
+ }
639
+ }
640
+ // We just use the highest known index no matter the wallet type.
641
+ //
642
+ // For entropy-based wallets (bip44), if a multichain account group with group index 1
643
+ // is inserted before another one with group index 0, then the naming will be:
644
+ // - "Account 1" (group index 1)
645
+ // - "Account 2" (group index 0)
646
+ // This naming makes more sense for the end-user.
647
+ //
648
+ // For other type of wallets, since those wallets can create arbitrary gaps, we still
649
+ // rely on the highest know index to avoid back-filling account with "old names".
650
+ let proposedNameIndex = Math.max(
651
+ // Use + 1 to use the next available index.
652
+ highestNameIndex + 1,
653
+ // In case all accounts have been renamed differently than the usual "Account <index>"
654
+ // pattern, we want to use the next "natural" index, which is just the number of groups
655
+ // in that wallet (e.g. ["Account A", "Another Account"], next natural index would be
656
+ // "Account 3" in this case).
657
+ nextNaturalNameIndex ?? Object.keys(wallet.groups).length);
658
+ // Find a unique name by checking for conflicts and incrementing if needed
659
+ let proposedNameExists;
660
+ let proposedName = '';
661
+ do {
662
+ proposedName = `${namePrefix} ${proposedNameIndex}`;
663
+ // Check if this name already exists in the wallet (excluding current group)
664
+ proposedNameExists = !(0, group_1.isAccountGroupNameUniqueFromWallet)(wallet, group.id, proposedName);
665
+ /* istanbul ignore next */
666
+ if (proposedNameExists) {
667
+ proposedNameIndex += 1; // Try next number
668
+ }
669
+ } while (proposedNameExists);
670
+ return proposedName;
671
+ }, _AccountTreeController_applyAccountGroupMetadata = function _AccountTreeController_applyAccountGroupMetadata(state, walletId, groupId, { allowComputedName, nextNaturalNameIndex, } = {}) {
588
672
  var _a;
589
673
  const wallet = state.accountTree.wallets[walletId];
590
674
  const group = wallet.groups[groupId];
@@ -595,62 +679,16 @@ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeControlle
595
679
  persistedGroupMetadata.name.value;
596
680
  }
597
681
  else if (!group.metadata.name) {
598
- // Get the appropriate rule for this wallet type
599
- const rule = __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_getRuleForWallet).call(this, wallet);
600
- // Get the prefix for groups of this wallet
601
- const namePrefix = rule.getDefaultAccountGroupPrefix(wallet);
602
- // Skip computed names for now - use default naming with per-wallet logic
603
- // TODO: Implement computed names in a future iteration
604
- // Parse the highest account index being used (similar to accounts-controller)
605
- let highestNameIndex = 0;
606
- for (const { id: otherGroupId } of Object.values(wallet.groups)) {
607
- // Skip the current group being processed
608
- if (otherGroupId === groupId) {
609
- continue;
610
- }
611
- // We always get the name from the persisted map, since `init` will clear the
612
- // `state.accountTree.wallets`, thus, given empty `group.metadata.name`.
613
- // NOTE: If the other group has not been named yet, we just use an empty name.
614
- const otherGroupName = state.accountGroupsMetadata[otherGroupId]?.name?.value ?? '';
615
- // Parse the existing group name to extract the numeric index
616
- const nameMatch = otherGroupName.match(/account\s+(\d+)$/iu);
617
- if (nameMatch) {
618
- const nameIndex = parseInt(nameMatch[1], 10);
619
- if (nameIndex > highestNameIndex) {
620
- highestNameIndex = nameIndex;
621
- }
622
- }
623
- }
624
- // We just use the highest known index no matter the wallet type.
625
- //
626
- // For entropy-based wallets (bip44), if a multichain account group with group index 1
627
- // is inserted before another one with group index 0, then the naming will be:
628
- // - "Account 1" (group index 1)
629
- // - "Account 2" (group index 0)
630
- // This naming makes more sense for the end-user.
631
- //
632
- // For other type of wallets, since those wallets can create arbitrary gaps, we still
633
- // rely on the highest know index to avoid back-filling account with "old names".
634
- let proposedNameIndex = Math.max(
635
- // Use + 1 to use the next available index.
636
- highestNameIndex + 1,
637
- // In case all accounts have been renamed differently than the usual "Account <index>"
638
- // pattern, we want to use the next "natural" index, which is just the number of groups
639
- // in that wallet (e.g. ["Account A", "Another Account"], next natural index would be
640
- // "Account 3" in this case).
641
- nextNaturalNameIndex ?? Object.keys(wallet.groups).length);
642
- // Find a unique name by checking for conflicts and incrementing if needed
643
- let proposedNameExists;
644
682
  let proposedName = '';
645
- do {
646
- proposedName = `${namePrefix} ${proposedNameIndex}`;
647
- // Check if this name already exists in the wallet (excluding current group)
648
- proposedNameExists = !(0, group_1.isAccountGroupNameUniqueFromWallet)(wallet, group.id, proposedName);
649
- /* istanbul ignore next */
650
- if (proposedNameExists) {
651
- proposedNameIndex += 1; // Try next number
652
- }
653
- } while (proposedNameExists);
683
+ // Computed names are usually only used for existing/old accounts. So this option
684
+ // should be used only when we first initialize the tree.
685
+ if (allowComputedName) {
686
+ proposedName = __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_getComputedAccountGroupName).call(this, wallet, group);
687
+ }
688
+ // If we still don't have a valid name candidate, we fallback to a default name.
689
+ if (!proposedName.length) {
690
+ proposedName = __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_getDefaultAccountGroupName).call(this, state, wallet, group, nextNaturalNameIndex);
691
+ }
654
692
  state.accountTree.wallets[walletId].groups[groupId].metadata.name =
655
693
  proposedName;
656
694
  (0, logger_1.projectLogger)(`[${group.id}] Set default name to: "${group.metadata.name}"`);
@@ -849,6 +887,23 @@ _AccountTreeController_accountIdToContext = new WeakMap(), _AccountTreeControlle
849
887
  }
850
888
  // Default to the default group in case of errors.
851
889
  return __classPrivateFieldGet(this, _AccountTreeController_instances, "m", _AccountTreeController_getDefaultAccountGroupId).call(this, wallets);
890
+ }, _AccountTreeController_handleSelectedAccountChange = function _AccountTreeController_handleSelectedAccountChange(account) {
891
+ const accountMapping = __classPrivateFieldGet(this, _AccountTreeController_accountIdToContext, "f").get(account.id);
892
+ if (!accountMapping) {
893
+ // Account not in tree yet, might be during initialization
894
+ return;
895
+ }
896
+ const { groupId } = accountMapping;
897
+ const previousSelectedAccountGroup = this.state.accountTree.selectedAccountGroup;
898
+ // Idempotent check - if the same group is already selected, do nothing
899
+ if (previousSelectedAccountGroup === groupId) {
900
+ return;
901
+ }
902
+ // Update selectedAccountGroup to match the selected account
903
+ this.update((state) => {
904
+ state.accountTree.selectedAccountGroup = groupId;
905
+ });
906
+ this.messagingSystem.publish(`${exports.controllerName}:selectedAccountGroupChange`, groupId, previousSelectedAccountGroup);
852
907
  }, _AccountTreeController_handleMultichainAccountWalletStatusChange = function _AccountTreeController_handleMultichainAccountWalletStatusChange(walletId, walletStatus) {
853
908
  this.update((state) => {
854
909
  const wallet = state.accountTree.wallets[walletId];