{"version":3,"file":"group.cjs","sourceRoot":"","sources":["../../../src/backup-and-sync/syncing/group.ts"],"names":[],"mappings":";;;AAAA,uDAAoE;AAGpE,6CAAmD;AAGnD,sDAA2D;AAE3D,wCAA8D;AAK9D,+EAG4C;AAC5C,8CAA0D;AAC1D,gDAAiD;AACjD,6CAAoD;AAEpD;;;;;;;;;;;GAWG;AACI,MAAM,kCAAkC,GAAG,KAAK,EACrD,OAA6B,EAC7B,eAAuB,EACvB,aAAqB,EACrB,SAAoB,EACpB,eAA6C,EAC9B,EAAE;IACjB,MAAM,6BAA6B,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,6DAA6D;IACtH,IAAA,4BAAmB,EACjB,YAAY,6BAA6B,+CAA+C,eAAe,EAAE,CAC1G,CAAC;IAEF,6EAA6E;IAC7E,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,IAAA,yCAA2B,EAAC,eAAe,CAAC,CAAC;IAC9D,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAClC,IAAA,sCAA8B,EAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,GAAG,CACnD,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CAC7C,CACF,CAAC;IAEF,IAAI,CAAC;QACH,yDAAyD;QACzD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,SAAS,CAAC,IAAI,CACzC,wDAAwD,EACxD;YACE,aAAa,EAAE,eAAe;YAC9B,cAAc,EAAE,CAAC;YACjB,YAAY,EAAE,aAAa;SAC5B,CACF,CAAC;QAEF,kDAAkD;QAClD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,kHAAkH;YAClH,uDAAuD;YACvD,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,oBAAoB,GAAG,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;gBAExE,IAAI,CAAC,oBAAoB,EAAE,CAAC;oBAC1B,OAAO,CAAC,oBAAoB,CAAC;wBAC3B,MAAM,EAAE,eAAe;wBACvB,SAAS;qBACV,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAA,4BAAmB,EAAC,wBAAwB,MAAM,CAAC,MAAM,iBAAiB,CAAC,CAAC;IAC9E,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,qEAAqE;QACrE,4DAA4D;QAC5D,yEAAyE;QACzE,oFAAoF;QACpF,oCAAoC;QACpC,gEAAgE;QAEhE,IAAA,4BAAmB,EACjB,wCAAwC;QACxC,uBAAuB;QACvB,IAAA,uBAAc,EAAC,KAAK,CAAC,CACtB,CAAC;IACJ,CAAC;AACH,CAAC,CAAC;AA/DW,QAAA,kCAAkC,sCA+D7C;AAEF;;;;;;;GAOG;AACI,KAAK,UAAU,gCAAgC,CACpD,OAA6B,EAC7B,qBAAqD,EACrD,eAAuB,EACvB,SAAoB;IAEpB,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAC5B,GAAG,qBAAqB,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAC1D,CAAC;IAEF,oEAAoE;IACpE,0CAA0C;IAC1C,6CAA6C;IAC7C,MAAM,IAAA,0CAAkC,EACtC,OAAO,EACP,eAAe,EACf,aAAa,EACb,SAAS,EACT,uCAA2B,CAAC,UAAU,CACvC,CAAC;AACJ,CAAC;AApBD,4EAoBC;AAED;;;;;;;;GAQG;AACH,KAAK,UAAU,qCAAqC,CAClD,OAA6B,EAC7B,UAA+C,EAC/C,oBAAqE,EACrE,SAAoB;IAEpB,MAAM,sBAAsB,GAC1B,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,qBAAqB,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC;IAEhE,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC1B,IAAA,4BAAmB,EACjB,SAAS,UAAU,CAAC,EAAE,4DAA4D,CACnF,CAAC;QAEF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sDAAsD;IACtD,IAAI,eAAe,GAAG,KAAK,CAAC;IAE5B,iCAAiC;IACjC,MAAM,iBAAiB,GAAG,MAAM,IAAA,iCAAsB,EAAC;QACrD,OAAO;QACP,aAAa,EAAE,sBAAsB,EAAE,IAAI;QAC3C,mBAAmB,EAAE,oBAAoB,CAAC,IAAI;QAC9C,wBAAwB,EAAE,CAAC,KAAK,EAAE,EAAE,CAClC,0CAAkC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC;QACvE,gBAAgB,EAAE,CAAC,IAAY,EAAE,EAAE;YACjC,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,UAAU,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACpE,CAAC;QACD,SAAS,EAAE;YACT,MAAM,EAAE,uCAA2B,CAAC,YAAY;YAChD,SAAS;SACV;KACF,CAAC,CAAC;IAEH,eAAe,KAAf,eAAe,GAAK,iBAAiB,EAAC;IAEtC,mCAAmC;IACnC,MAAM,mBAAmB,GAAG,MAAM,IAAA,iCAAsB,EAAC;QACvD,OAAO;QACP,aAAa,EAAE,sBAAsB,EAAE,MAAM;QAC7C,mBAAmB,EAAE,oBAAoB,CAAC,MAAM;QAChD,wBAAwB,EAAE,CAAC,KAAK,EAAE,EAAE,CAClC,0CAAkC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC;QACzE,gBAAgB,EAAE,CAAC,MAAe,EAAE,EAAE;YACpC,OAAO,CAAC,UAAU,CAAC,qBAAqB,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAClE,CAAC;QACD,SAAS,EAAE;YACT,MAAM,EAAE,uCAA2B,CAAC,wBAAwB;YAC5D,SAAS;SACV;KACF,CAAC,CAAC;IAEH,eAAe,KAAf,eAAe,GAAK,mBAAmB,EAAC;IAExC,mCAAmC;IACnC,MAAM,mBAAmB,GAAG,MAAM,IAAA,iCAAsB,EAAC;QACvD,OAAO;QACP,aAAa,EAAE,sBAAsB,EAAE,MAAM;QAC7C,mBAAmB,EAAE,oBAAoB,CAAC,MAAM;QAChD,wBAAwB,EAAE,CAAC,KAAK,EAAE,EAAE,CAClC,0CAAkC,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC;QACzE,gBAAgB,EAAE,CAAC,MAAe,EAAE,EAAE;YACpC,OAAO,CAAC,UAAU,CAAC,qBAAqB,CAAC,UAAU,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QAClE,CAAC;QACD,SAAS,EAAE;YACT,MAAM,EAAE,uCAA2B,CAAC,wBAAwB;YAC5D,SAAS;SACV;KACF,CAAC,CAAC;IAEH,eAAe,KAAf,eAAe,GAAK,mBAAmB,EAAC;IAExC,OAAO,eAAe,CAAC;AACzB,CAAC;AAED;;;;;;;;GAQG;AACI,KAAK,UAAU,iBAAiB,CACrC,OAA6B,EAC7B,UAA+C,EAC/C,oBAAyD,EACzD,eAAuB,EACvB,SAAoB;IAEpB,MAAM,eAAe,GAAG,MAAM,qCAAqC,CACjE,OAAO,EACP,UAAU,EACV,oBAAoB,EACpB,SAAS,CACV,CAAC;IAEF,IAAI,eAAe,EAAE,CAAC;QACpB,MAAM,IAAA,2CAAsB,EAAC,OAAO,EAAE,UAAU,EAAE,eAAe,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAjBD,8CAiBC;AAED;;;;;;;;GAQG;AACI,KAAK,UAAU,kBAAkB,CACtC,OAA6B,EAC7B,MAAkC,EAClC,qBAAqD,EACrD,eAAuB,EACvB,SAAoB;IAEpB,MAAM,0CAA0C,GAC9C,EAAE,CAAC;IAEL,MAAM,mBAAmB,GAAG,IAAA,sCAA8B,EACxD,OAAO,EACP,MAAM,CAAC,EAAE,CACV,CAAC;IAEF,KAAK,MAAM,kBAAkB,IAAI,mBAAmB,EAAE,CAAC;QACrD,MAAM,oBAAoB,GAAG,qBAAqB,CAAC,IAAI,CACrD,CAAC,KAAK,EAAE,EAAE,CACR,KAAK,CAAC,UAAU,KAAK,kBAAkB,CAAC,QAAQ,CAAC,OAAO,CAAC,UAAU,CACtE,CAAC;QAEF,MAAM,eAAe,GAAG,MAAM,qCAAqC,CACjE,OAAO,EACP,kBAAkB,EAClB,oBAAoB,EACpB,SAAS,CACV,CAAC;QAEF,uEAAuE;QACvE,IAAI,eAAe,EAAE,CAAC;YACpB,0CAA0C,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAED,0DAA0D;IAC1D,IAAI,0CAA0C,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAA,gDAA2B,EAC/B,OAAO,EACP,0CAA0C,EAC1C,eAAe,CAChB,CAAC;IACJ,CAAC;AACH,CAAC;AA1CD,gDA0CC","sourcesContent":["import { toMultichainAccountWalletId } from '@metamask/account-api';\n\nimport type { AccountGroupMultichainAccountObject } from '../../group';\nimport { backupAndSyncLogger } from '../../logger';\nimport type { AccountWalletEntropyObject } from '../../wallet';\nimport type { BackupAndSyncAnalyticsAction } from '../analytics';\nimport { BackupAndSyncAnalyticsEvent } from '../analytics';\nimport type { ProfileId } from '../authentication';\nimport { UserStorageSyncedWalletGroupSchema } from '../types';\nimport type {\n  BackupAndSyncContext,\n  UserStorageSyncedWalletGroup,\n} from '../types';\nimport {\n  pushGroupToUserStorage,\n  pushGroupToUserStorageBatch,\n} from '../user-storage/network-operations';\nimport { getLocalGroupsForEntropyWallet } from '../utils';\nimport { toErrorMessage } from '../utils/errors';\nimport { compareAndSyncMetadata } from './metadata';\n\n/**\n * Creates multiple multichain account groups in batch (from 0 to maxGroupIndex).\n * This is an optimized version that creates all groups in one operation instead of\n * creating them sequentially.\n *\n * @param context - The sync context containing controller and messenger.\n * @param entropySourceId - The entropy source ID.\n * @param maxGroupIndex - Number of account groups to create in batch.\n * @param profileId - The profile ID for analytics.\n * @param analyticsAction - The analytics action to log for each created group.\n * @returns Array of created group IDs.\n */\nexport const createMultichainAccountGroupsBatch = async (\n  context: BackupAndSyncContext,\n  entropySourceId: string,\n  maxGroupIndex: number,\n  profileId: ProfileId,\n  analyticsAction: BackupAndSyncAnalyticsAction,\n): Promise<void> => {\n  const numberOfAccountGroupsToCreate = maxGroupIndex + 1; // maxGroupIndex is zero-based, so we add 1 to get the count.\n  backupAndSyncLogger(\n    `Creating ${numberOfAccountGroupsToCreate} account groups (batch) for entropy source: ${entropySourceId}`,\n  );\n\n  // Capture the set of group indices that already exist before the batch call,\n  // so we can correctly identify newly created groups after the call completes.\n  const walletId = toMultichainAccountWalletId(entropySourceId);\n  const existingGroupIndices = new Set(\n    getLocalGroupsForEntropyWallet(context, walletId).map(\n      (group) => group.metadata.entropy.groupIndex,\n    ),\n  );\n\n  try {\n    // Call the batched creation method (this is idempotent).\n    const groups = await context.messenger.call(\n      'MultichainAccountService:createMultichainAccountGroups',\n      {\n        entropySource: entropySourceId,\n        fromGroupIndex: 0,\n        toGroupIndex: maxGroupIndex,\n      },\n    );\n\n    // Contains all groups (existing + newly created).\n    for (const group of groups) {\n      // TODO: A group should not be null here, but EVM provider might fail to create some groups sometimes, which means\n      // we can end up having an \"empty group\" for some time.\n      if (group) {\n        const didGroupAlreadyExist = existingGroupIndices.has(group.groupIndex);\n\n        if (!didGroupAlreadyExist) {\n          context.emitAnalyticsEventFn({\n            action: analyticsAction,\n            profileId,\n          });\n        }\n      }\n    }\n\n    backupAndSyncLogger(`Successfully created ${groups.length} groups (batch)`);\n  } catch (error) {\n    // This can happen if the Snap Keyring is not ready yet when invoking\n    // `MultichainAccountService:createMultichainAccountGroups`.\n    // Since `MultichainAccountService:createMultichainAccountGroups` will at\n    // least create the EVM account and the account group before throwing, we can safely\n    // ignore this error and swallow it.\n    // Any missing Snap accounts will be added later with alignment.\n\n    backupAndSyncLogger(\n      `Failed to create account groups batch:`,\n      // istanbul ignore next\n      toErrorMessage(error),\n    );\n  }\n};\n\n/**\n * Creates local groups from user storage groups.\n *\n * @param context - The sync context containing controller and messenger.\n * @param groupsFromUserStorage - Array of groups from user storage.\n * @param entropySourceId - The entropy source ID.\n * @param profileId - The profile ID for analytics.\n */\nexport async function createLocalGroupsFromUserStorage(\n  context: BackupAndSyncContext,\n  groupsFromUserStorage: UserStorageSyncedWalletGroup[],\n  entropySourceId: string,\n  profileId: ProfileId,\n): Promise<void> {\n  const maxGroupIndex = Math.max(\n    ...groupsFromUserStorage.map((group) => group.groupIndex),\n  );\n\n  // Creating multichain account group is idempotent, so we can safely\n  // re-create every groups starting from 0.\n  // Use batch creation for better performance.\n  await createMultichainAccountGroupsBatch(\n    context,\n    entropySourceId,\n    maxGroupIndex,\n    profileId,\n    BackupAndSyncAnalyticsEvent.GroupAdded,\n  );\n}\n\n/**\n * Syncs group metadata fields and determines if the group needs to be pushed to user storage.\n *\n * @param context - The sync context containing controller and messenger.\n * @param localGroup - The local group to sync.\n * @param groupFromUserStorage - The group from user storage to compare against.\n * @param profileId - The profile ID for analytics.\n * @returns A promise that resolves to true if the group needs to be pushed to user storage.\n */\nasync function syncGroupMetadataAndCheckIfPushNeeded(\n  context: BackupAndSyncContext,\n  localGroup: AccountGroupMultichainAccountObject,\n  groupFromUserStorage: UserStorageSyncedWalletGroup | null | undefined,\n  profileId: ProfileId,\n): Promise<boolean> {\n  const groupPersistedMetadata =\n    context.controller.state.accountGroupsMetadata[localGroup.id];\n\n  if (!groupFromUserStorage) {\n    backupAndSyncLogger(\n      `Group ${localGroup.id} did not exist in user storage, pushing to user storage...`,\n    );\n\n    return true;\n  }\n\n  // Track if we need to push this group to user storage\n  let shouldPushGroup = false;\n\n  // Compare and sync name metadata\n  const shouldPushForName = await compareAndSyncMetadata({\n    context,\n    localMetadata: groupPersistedMetadata?.name,\n    userStorageMetadata: groupFromUserStorage.name,\n    validateUserStorageValue: (value) =>\n      UserStorageSyncedWalletGroupSchema.schema.name.schema.value.is(value),\n    applyLocalUpdate: (name: string) => {\n      context.controller.setAccountGroupName(localGroup.id, name, true);\n    },\n    analytics: {\n      action: BackupAndSyncAnalyticsEvent.GroupRenamed,\n      profileId,\n    },\n  });\n\n  shouldPushGroup ||= shouldPushForName;\n\n  // Compare and sync pinned metadata\n  const shouldPushForPinned = await compareAndSyncMetadata({\n    context,\n    localMetadata: groupPersistedMetadata?.pinned,\n    userStorageMetadata: groupFromUserStorage.pinned,\n    validateUserStorageValue: (value) =>\n      UserStorageSyncedWalletGroupSchema.schema.pinned.schema.value.is(value),\n    applyLocalUpdate: (pinned: boolean) => {\n      context.controller.setAccountGroupPinned(localGroup.id, pinned);\n    },\n    analytics: {\n      action: BackupAndSyncAnalyticsEvent.GroupPinnedStatusChanged,\n      profileId,\n    },\n  });\n\n  shouldPushGroup ||= shouldPushForPinned;\n\n  // Compare and sync hidden metadata\n  const shouldPushForHidden = await compareAndSyncMetadata({\n    context,\n    localMetadata: groupPersistedMetadata?.hidden,\n    userStorageMetadata: groupFromUserStorage.hidden,\n    validateUserStorageValue: (value) =>\n      UserStorageSyncedWalletGroupSchema.schema.hidden.schema.value.is(value),\n    applyLocalUpdate: (hidden: boolean) => {\n      context.controller.setAccountGroupHidden(localGroup.id, hidden);\n    },\n    analytics: {\n      action: BackupAndSyncAnalyticsEvent.GroupHiddenStatusChanged,\n      profileId,\n    },\n  });\n\n  shouldPushGroup ||= shouldPushForHidden;\n\n  return shouldPushGroup;\n}\n\n/**\n * Syncs a single group's metadata between local and user storage.\n *\n * @param context - The sync context containing controller and messenger.\n * @param localGroup - The local group to sync.\n * @param groupFromUserStorage - The group from user storage to compare against (or null if it doesn't exist).\n * @param entropySourceId - The entropy source ID.\n * @param profileId - The profile ID for analytics.\n */\nexport async function syncGroupMetadata(\n  context: BackupAndSyncContext,\n  localGroup: AccountGroupMultichainAccountObject,\n  groupFromUserStorage: UserStorageSyncedWalletGroup | null,\n  entropySourceId: string,\n  profileId: ProfileId,\n): Promise<void> {\n  const shouldPushGroup = await syncGroupMetadataAndCheckIfPushNeeded(\n    context,\n    localGroup,\n    groupFromUserStorage,\n    profileId,\n  );\n\n  if (shouldPushGroup) {\n    await pushGroupToUserStorage(context, localGroup, entropySourceId);\n  }\n}\n\n/**\n * Syncs group metadata between local and user storage.\n *\n * @param context - The sync context containing controller and messenger.\n * @param wallet - The local wallet containing the groups.\n * @param groupsFromUserStorage - Array of groups from user storage.\n * @param entropySourceId - The entropy source ID.\n * @param profileId - The profile ID for analytics.\n */\nexport async function syncGroupsMetadata(\n  context: BackupAndSyncContext,\n  wallet: AccountWalletEntropyObject,\n  groupsFromUserStorage: UserStorageSyncedWalletGroup[],\n  entropySourceId: string,\n  profileId: ProfileId,\n): Promise<void> {\n  const localSyncableGroupsToBePushedToUserStorage: AccountGroupMultichainAccountObject[] =\n    [];\n\n  const localSyncableGroups = getLocalGroupsForEntropyWallet(\n    context,\n    wallet.id,\n  );\n\n  for (const localSyncableGroup of localSyncableGroups) {\n    const groupFromUserStorage = groupsFromUserStorage.find(\n      (group) =>\n        group.groupIndex === localSyncableGroup.metadata.entropy.groupIndex,\n    );\n\n    const shouldPushGroup = await syncGroupMetadataAndCheckIfPushNeeded(\n      context,\n      localSyncableGroup,\n      groupFromUserStorage,\n      profileId,\n    );\n\n    // Add to push list if any metadata needs to be updated in user storage\n    if (shouldPushGroup) {\n      localSyncableGroupsToBePushedToUserStorage.push(localSyncableGroup);\n    }\n  }\n\n  // Push all groups that need to be updated to user storage\n  if (localSyncableGroupsToBePushedToUserStorage.length > 0) {\n    await pushGroupToUserStorageBatch(\n      context,\n      localSyncableGroupsToBePushedToUserStorage,\n      entropySourceId,\n    );\n  }\n}\n"]}