@metamask/profile-sync-controller 19.0.0 → 21.0.0

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 (186) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/dist/controllers/authentication/AuthenticationController.cjs +21 -9
  3. package/dist/controllers/authentication/AuthenticationController.cjs.map +1 -1
  4. package/dist/controllers/authentication/AuthenticationController.d.cts +10 -3
  5. package/dist/controllers/authentication/AuthenticationController.d.cts.map +1 -1
  6. package/dist/controllers/authentication/AuthenticationController.d.mts +10 -3
  7. package/dist/controllers/authentication/AuthenticationController.d.mts.map +1 -1
  8. package/dist/controllers/authentication/AuthenticationController.mjs +21 -9
  9. package/dist/controllers/authentication/AuthenticationController.mjs.map +1 -1
  10. package/dist/controllers/user-storage/UserStorageController.cjs +24 -47
  11. package/dist/controllers/user-storage/UserStorageController.cjs.map +1 -1
  12. package/dist/controllers/user-storage/UserStorageController.d.cts +16 -49
  13. package/dist/controllers/user-storage/UserStorageController.d.cts.map +1 -1
  14. package/dist/controllers/user-storage/UserStorageController.d.mts +16 -49
  15. package/dist/controllers/user-storage/UserStorageController.d.mts.map +1 -1
  16. package/dist/controllers/user-storage/UserStorageController.mjs +24 -47
  17. package/dist/controllers/user-storage/UserStorageController.mjs.map +1 -1
  18. package/dist/controllers/user-storage/account-syncing/controller-integration.cjs +185 -166
  19. package/dist/controllers/user-storage/account-syncing/controller-integration.cjs.map +1 -1
  20. package/dist/controllers/user-storage/account-syncing/controller-integration.d.cts +3 -0
  21. package/dist/controllers/user-storage/account-syncing/controller-integration.d.cts.map +1 -1
  22. package/dist/controllers/user-storage/account-syncing/controller-integration.d.mts +3 -0
  23. package/dist/controllers/user-storage/account-syncing/controller-integration.d.mts.map +1 -1
  24. package/dist/controllers/user-storage/account-syncing/controller-integration.mjs +185 -166
  25. package/dist/controllers/user-storage/account-syncing/controller-integration.mjs.map +1 -1
  26. package/dist/controllers/user-storage/account-syncing/types.cjs.map +1 -1
  27. package/dist/controllers/user-storage/account-syncing/types.d.cts +2 -0
  28. package/dist/controllers/user-storage/account-syncing/types.d.cts.map +1 -1
  29. package/dist/controllers/user-storage/account-syncing/types.d.mts +2 -0
  30. package/dist/controllers/user-storage/account-syncing/types.d.mts.map +1 -1
  31. package/dist/controllers/user-storage/account-syncing/types.mjs.map +1 -1
  32. package/dist/controllers/user-storage/constants.cjs +14 -1
  33. package/dist/controllers/user-storage/constants.cjs.map +1 -1
  34. package/dist/controllers/user-storage/constants.d.cts +11 -0
  35. package/dist/controllers/user-storage/constants.d.cts.map +1 -1
  36. package/dist/controllers/user-storage/constants.d.mts +11 -0
  37. package/dist/controllers/user-storage/constants.d.mts.map +1 -1
  38. package/dist/controllers/user-storage/constants.mjs +13 -0
  39. package/dist/controllers/user-storage/constants.mjs.map +1 -1
  40. package/dist/controllers/user-storage/contact-syncing/controller-integration.cjs +227 -162
  41. package/dist/controllers/user-storage/contact-syncing/controller-integration.cjs.map +1 -1
  42. package/dist/controllers/user-storage/contact-syncing/controller-integration.d.cts +3 -0
  43. package/dist/controllers/user-storage/contact-syncing/controller-integration.d.cts.map +1 -1
  44. package/dist/controllers/user-storage/contact-syncing/controller-integration.d.mts +3 -0
  45. package/dist/controllers/user-storage/contact-syncing/controller-integration.d.mts.map +1 -1
  46. package/dist/controllers/user-storage/contact-syncing/controller-integration.mjs +227 -162
  47. package/dist/controllers/user-storage/contact-syncing/controller-integration.mjs.map +1 -1
  48. package/dist/controllers/user-storage/contact-syncing/types.cjs.map +1 -1
  49. package/dist/controllers/user-storage/contact-syncing/types.d.cts +2 -0
  50. package/dist/controllers/user-storage/contact-syncing/types.d.cts.map +1 -1
  51. package/dist/controllers/user-storage/contact-syncing/types.d.mts +2 -0
  52. package/dist/controllers/user-storage/contact-syncing/types.d.mts.map +1 -1
  53. package/dist/controllers/user-storage/contact-syncing/types.mjs.map +1 -1
  54. package/dist/controllers/user-storage/mocks/mockResponses.cjs +1 -1
  55. package/dist/controllers/user-storage/mocks/mockResponses.cjs.map +1 -1
  56. package/dist/controllers/user-storage/mocks/mockResponses.d.cts +9 -9
  57. package/dist/controllers/user-storage/mocks/mockResponses.d.cts.map +1 -1
  58. package/dist/controllers/user-storage/mocks/mockResponses.d.mts +9 -9
  59. package/dist/controllers/user-storage/mocks/mockResponses.d.mts.map +1 -1
  60. package/dist/controllers/user-storage/mocks/mockResponses.mjs +1 -1
  61. package/dist/controllers/user-storage/mocks/mockResponses.mjs.map +1 -1
  62. package/dist/controllers/user-storage/types.cjs.map +1 -1
  63. package/dist/controllers/user-storage/types.d.cts +3 -3
  64. package/dist/controllers/user-storage/types.d.cts.map +1 -1
  65. package/dist/controllers/user-storage/types.d.mts +3 -3
  66. package/dist/controllers/user-storage/types.d.mts.map +1 -1
  67. package/dist/controllers/user-storage/types.mjs.map +1 -1
  68. package/dist/sdk/authentication-jwt-bearer/flow-siwe.cjs +4 -0
  69. package/dist/sdk/authentication-jwt-bearer/flow-siwe.cjs.map +1 -1
  70. package/dist/sdk/authentication-jwt-bearer/flow-siwe.d.cts +2 -1
  71. package/dist/sdk/authentication-jwt-bearer/flow-siwe.d.cts.map +1 -1
  72. package/dist/sdk/authentication-jwt-bearer/flow-siwe.d.mts +2 -1
  73. package/dist/sdk/authentication-jwt-bearer/flow-siwe.d.mts.map +1 -1
  74. package/dist/sdk/authentication-jwt-bearer/flow-siwe.mjs +5 -1
  75. package/dist/sdk/authentication-jwt-bearer/flow-siwe.mjs.map +1 -1
  76. package/dist/sdk/authentication-jwt-bearer/flow-srp.cjs +4 -0
  77. package/dist/sdk/authentication-jwt-bearer/flow-srp.cjs.map +1 -1
  78. package/dist/sdk/authentication-jwt-bearer/flow-srp.d.cts +2 -1
  79. package/dist/sdk/authentication-jwt-bearer/flow-srp.d.cts.map +1 -1
  80. package/dist/sdk/authentication-jwt-bearer/flow-srp.d.mts +2 -1
  81. package/dist/sdk/authentication-jwt-bearer/flow-srp.d.mts.map +1 -1
  82. package/dist/sdk/authentication-jwt-bearer/flow-srp.mjs +5 -1
  83. package/dist/sdk/authentication-jwt-bearer/flow-srp.mjs.map +1 -1
  84. package/dist/sdk/authentication-jwt-bearer/services.cjs +33 -1
  85. package/dist/sdk/authentication-jwt-bearer/services.cjs.map +1 -1
  86. package/dist/sdk/authentication-jwt-bearer/services.d.cts +10 -1
  87. package/dist/sdk/authentication-jwt-bearer/services.d.cts.map +1 -1
  88. package/dist/sdk/authentication-jwt-bearer/services.d.mts +10 -1
  89. package/dist/sdk/authentication-jwt-bearer/services.d.mts.map +1 -1
  90. package/dist/sdk/authentication-jwt-bearer/services.mjs +30 -0
  91. package/dist/sdk/authentication-jwt-bearer/services.mjs.map +1 -1
  92. package/dist/sdk/authentication-jwt-bearer/types.cjs.map +1 -1
  93. package/dist/sdk/authentication-jwt-bearer/types.d.cts +11 -0
  94. package/dist/sdk/authentication-jwt-bearer/types.d.cts.map +1 -1
  95. package/dist/sdk/authentication-jwt-bearer/types.d.mts +11 -0
  96. package/dist/sdk/authentication-jwt-bearer/types.d.mts.map +1 -1
  97. package/dist/sdk/authentication-jwt-bearer/types.mjs.map +1 -1
  98. package/dist/sdk/authentication.cjs +3 -0
  99. package/dist/sdk/authentication.cjs.map +1 -1
  100. package/dist/sdk/authentication.d.cts +2 -1
  101. package/dist/sdk/authentication.d.cts.map +1 -1
  102. package/dist/sdk/authentication.d.mts +2 -1
  103. package/dist/sdk/authentication.d.mts.map +1 -1
  104. package/dist/sdk/authentication.mjs +3 -0
  105. package/dist/sdk/authentication.mjs.map +1 -1
  106. package/dist/sdk/mocks/auth.cjs +22 -1
  107. package/dist/sdk/mocks/auth.cjs.map +1 -1
  108. package/dist/sdk/mocks/auth.d.cts +13 -0
  109. package/dist/sdk/mocks/auth.d.cts.map +1 -1
  110. package/dist/sdk/mocks/auth.d.mts +13 -0
  111. package/dist/sdk/mocks/auth.d.mts.map +1 -1
  112. package/dist/sdk/mocks/auth.mjs +23 -2
  113. package/dist/sdk/mocks/auth.mjs.map +1 -1
  114. package/dist/sdk/user-storage.cjs +3 -9
  115. package/dist/sdk/user-storage.cjs.map +1 -1
  116. package/dist/sdk/user-storage.d.cts +0 -1
  117. package/dist/sdk/user-storage.d.cts.map +1 -1
  118. package/dist/sdk/user-storage.d.mts +0 -1
  119. package/dist/sdk/user-storage.d.mts.map +1 -1
  120. package/dist/sdk/user-storage.mjs +3 -9
  121. package/dist/sdk/user-storage.mjs.map +1 -1
  122. package/dist/shared/storage-schema.cjs +9 -32
  123. package/dist/shared/storage-schema.cjs.map +1 -1
  124. package/dist/shared/storage-schema.d.cts +10 -42
  125. package/dist/shared/storage-schema.d.cts.map +1 -1
  126. package/dist/shared/storage-schema.d.mts +10 -42
  127. package/dist/shared/storage-schema.d.mts.map +1 -1
  128. package/dist/shared/storage-schema.mjs +8 -31
  129. package/dist/shared/storage-schema.mjs.map +1 -1
  130. package/package.json +6 -8
  131. package/dist/controllers/user-storage/network-syncing/add-network-utils.cjs +0 -36
  132. package/dist/controllers/user-storage/network-syncing/add-network-utils.cjs.map +0 -1
  133. package/dist/controllers/user-storage/network-syncing/add-network-utils.d.cts +0 -22
  134. package/dist/controllers/user-storage/network-syncing/add-network-utils.d.cts.map +0 -1
  135. package/dist/controllers/user-storage/network-syncing/add-network-utils.d.mts +0 -22
  136. package/dist/controllers/user-storage/network-syncing/add-network-utils.d.mts.map +0 -1
  137. package/dist/controllers/user-storage/network-syncing/add-network-utils.mjs +0 -31
  138. package/dist/controllers/user-storage/network-syncing/add-network-utils.mjs.map +0 -1
  139. package/dist/controllers/user-storage/network-syncing/controller-integration.cjs +0 -193
  140. package/dist/controllers/user-storage/network-syncing/controller-integration.cjs.map +0 -1
  141. package/dist/controllers/user-storage/network-syncing/controller-integration.d.cts +0 -57
  142. package/dist/controllers/user-storage/network-syncing/controller-integration.d.cts.map +0 -1
  143. package/dist/controllers/user-storage/network-syncing/controller-integration.d.mts +0 -57
  144. package/dist/controllers/user-storage/network-syncing/controller-integration.d.mts.map +0 -1
  145. package/dist/controllers/user-storage/network-syncing/controller-integration.mjs +0 -191
  146. package/dist/controllers/user-storage/network-syncing/controller-integration.mjs.map +0 -1
  147. package/dist/controllers/user-storage/network-syncing/services.cjs +0 -79
  148. package/dist/controllers/user-storage/network-syncing/services.cjs.map +0 -1
  149. package/dist/controllers/user-storage/network-syncing/services.d.cts +0 -34
  150. package/dist/controllers/user-storage/network-syncing/services.d.cts.map +0 -1
  151. package/dist/controllers/user-storage/network-syncing/services.d.mts +0 -34
  152. package/dist/controllers/user-storage/network-syncing/services.d.mts.map +0 -1
  153. package/dist/controllers/user-storage/network-syncing/services.mjs +0 -73
  154. package/dist/controllers/user-storage/network-syncing/services.mjs.map +0 -1
  155. package/dist/controllers/user-storage/network-syncing/sync-all.cjs +0 -153
  156. package/dist/controllers/user-storage/network-syncing/sync-all.cjs.map +0 -1
  157. package/dist/controllers/user-storage/network-syncing/sync-all.d.cts +0 -32
  158. package/dist/controllers/user-storage/network-syncing/sync-all.d.cts.map +0 -1
  159. package/dist/controllers/user-storage/network-syncing/sync-all.d.mts +0 -32
  160. package/dist/controllers/user-storage/network-syncing/sync-all.d.mts.map +0 -1
  161. package/dist/controllers/user-storage/network-syncing/sync-all.mjs +0 -145
  162. package/dist/controllers/user-storage/network-syncing/sync-all.mjs.map +0 -1
  163. package/dist/controllers/user-storage/network-syncing/sync-mutations.cjs +0 -28
  164. package/dist/controllers/user-storage/network-syncing/sync-mutations.cjs.map +0 -1
  165. package/dist/controllers/user-storage/network-syncing/sync-mutations.d.cts +0 -17
  166. package/dist/controllers/user-storage/network-syncing/sync-mutations.d.cts.map +0 -1
  167. package/dist/controllers/user-storage/network-syncing/sync-mutations.d.mts +0 -17
  168. package/dist/controllers/user-storage/network-syncing/sync-mutations.d.mts.map +0 -1
  169. package/dist/controllers/user-storage/network-syncing/sync-mutations.mjs +0 -22
  170. package/dist/controllers/user-storage/network-syncing/sync-mutations.mjs.map +0 -1
  171. package/dist/controllers/user-storage/network-syncing/types.cjs +0 -16
  172. package/dist/controllers/user-storage/network-syncing/types.cjs.map +0 -1
  173. package/dist/controllers/user-storage/network-syncing/types.d.cts +0 -15
  174. package/dist/controllers/user-storage/network-syncing/types.d.cts.map +0 -1
  175. package/dist/controllers/user-storage/network-syncing/types.d.mts +0 -15
  176. package/dist/controllers/user-storage/network-syncing/types.d.mts.map +0 -1
  177. package/dist/controllers/user-storage/network-syncing/types.mjs +0 -11
  178. package/dist/controllers/user-storage/network-syncing/types.mjs.map +0 -1
  179. package/dist/controllers/user-storage/network-syncing/update-network-utils.cjs +0 -129
  180. package/dist/controllers/user-storage/network-syncing/update-network-utils.cjs.map +0 -1
  181. package/dist/controllers/user-storage/network-syncing/update-network-utils.d.cts +0 -59
  182. package/dist/controllers/user-storage/network-syncing/update-network-utils.d.cts.map +0 -1
  183. package/dist/controllers/user-storage/network-syncing/update-network-utils.d.mts +0 -59
  184. package/dist/controllers/user-storage/network-syncing/update-network-utils.d.mts.map +0 -1
  185. package/dist/controllers/user-storage/network-syncing/update-network-utils.mjs +0 -122
  186. package/dist/controllers/user-storage/network-syncing/update-network-utils.mjs.map +0 -1
@@ -2,6 +2,7 @@ import { canPerformContactSyncing } from "./sync-utils.mjs";
2
2
  import { mapAddressBookEntryToUserStorageEntry, mapUserStorageEntryToAddressBookEntry } from "./utils.mjs";
3
3
  import { isContactBridgedFromAccounts } from "./utils.mjs";
4
4
  import { USER_STORAGE_FEATURE_NAMES } from "../../../shared/storage-schema.mjs";
5
+ import { TraceName } from "../constants.mjs";
5
6
  /**
6
7
  * Creates a unique key for a contact based on chainId and address
7
8
  *
@@ -32,138 +33,163 @@ function createContactKey(contact) {
32
33
  *
33
34
  * @param config - Parameters used for syncing callbacks
34
35
  * @param options - Parameters used for syncing operations
36
+ * @returns Promise that resolves when contact synchronization is complete
35
37
  */
36
38
  export async function syncContactsWithUserStorage(config, options) {
37
- const { getMessenger, getUserStorageControllerInstance } = options;
39
+ const { getMessenger, getUserStorageControllerInstance, trace } = options;
38
40
  const { onContactSyncErroneousSituation, onContactUpdated, onContactDeleted, } = config;
39
- try {
40
- // Cannot perform sync, conditions not met
41
- if (!canPerformContactSyncing(options)) {
42
- return;
43
- }
44
- // Activate sync semaphore to prevent event loops
45
- await getUserStorageControllerInstance().setIsContactSyncingInProgress(true);
46
- // Get all local contacts from AddressBookController (exclude chain "*" contacts)
47
- const localVisibleContacts = getMessenger()
48
- .call('AddressBookController:list')
49
- .filter((contact) => !isContactBridgedFromAccounts(contact))
50
- .filter((contact) => contact.address && contact.chainId && contact.name?.trim()) || [];
51
- // Get remote contacts from user storage API
52
- const remoteContacts = await getRemoteContacts(options);
53
- // Filter remote contacts to exclude invalid ones (or empty array if no remote contacts)
54
- const validRemoteContacts = remoteContacts?.filter((contact) => contact.address && contact.chainId && contact.name?.trim()) || [];
55
- // Prepare maps for efficient lookup
56
- const localContactsMap = new Map();
57
- const remoteContactsMap = new Map();
58
- localVisibleContacts.forEach((contact) => {
59
- const key = createContactKey(contact);
60
- localContactsMap.set(key, contact);
61
- });
62
- validRemoteContacts.forEach((contact) => {
63
- const key = createContactKey(contact);
64
- remoteContactsMap.set(key, contact);
65
- });
66
- // Lists to track contacts that need to be synced
67
- const contactsToAddOrUpdateLocally = [];
68
- const contactsToDeleteLocally = [];
69
- const contactsToUpdateRemotely = [];
70
- // SCENARIO 2 & 6: Process remote contacts - handle new device sync and remote updates
71
- for (const remoteContact of validRemoteContacts) {
72
- const key = createContactKey(remoteContact);
73
- const localContact = localContactsMap.get(key);
74
- // Handle remote contact based on its status and local existence
75
- if (remoteContact.deletedAt) {
76
- // SCENARIO 8: Remote deletion - should be applied locally if contact exists locally
77
- if (localContact) {
78
- contactsToDeleteLocally.push(remoteContact);
79
- }
80
- }
81
- else if (!localContact) {
82
- // SCENARIO 2: New contact from remote - import to local
83
- contactsToAddOrUpdateLocally.push(remoteContact);
84
- }
85
- else {
86
- // SCENARIO 4 & 6: Contact exists on both sides - check for conflicts
87
- const hasContentDifference = localContact.name !== remoteContact.name ||
88
- localContact.memo !== remoteContact.memo;
89
- if (hasContentDifference) {
90
- // Check timestamps to determine which version to keep
91
- const localTimestamp = localContact.lastUpdatedAt || 0;
92
- const remoteTimestamp = remoteContact.lastUpdatedAt || 0;
93
- if (localTimestamp >= remoteTimestamp) {
94
- // Local is newer (or same age) - use local version
95
- contactsToUpdateRemotely.push(localContact);
41
+ // Cannot perform sync, conditions not met
42
+ if (!canPerformContactSyncing(options)) {
43
+ return;
44
+ }
45
+ // NOTE: Pre-sync operations (canPerformContactSyncing, AddressBookController:list, getRemoteContacts)
46
+ // are intentionally outside try-catch to let errors bubble up to Sentry for better debugging.
47
+ // Only "erroneous situation" errors during sync logic itself should be caught.
48
+ // Get all local contacts from AddressBookController (exclude chain "*" contacts)
49
+ const localVisibleContacts = getMessenger()
50
+ .call('AddressBookController:list')
51
+ .filter((contact) => !isContactBridgedFromAccounts(contact))
52
+ .filter((contact) => contact.address && contact.chainId && contact.name?.trim()) || [];
53
+ // Get remote contacts from user storage API
54
+ const remoteContacts = await getRemoteContacts(options);
55
+ // Filter remote contacts to exclude invalid ones (or empty array if no remote contacts)
56
+ const validRemoteContacts = remoteContacts?.filter((contact) => contact.address && contact.chainId && contact.name?.trim()) || [];
57
+ const performSync = async () => {
58
+ try {
59
+ // Activate sync semaphore to prevent event loops
60
+ await getUserStorageControllerInstance().setIsContactSyncingInProgress(true);
61
+ // Prepare maps for efficient lookup
62
+ const localContactsMap = new Map();
63
+ const remoteContactsMap = new Map();
64
+ localVisibleContacts.forEach((contact) => {
65
+ const key = createContactKey(contact);
66
+ localContactsMap.set(key, contact);
67
+ });
68
+ validRemoteContacts.forEach((contact) => {
69
+ const key = createContactKey(contact);
70
+ remoteContactsMap.set(key, contact);
71
+ });
72
+ // Lists to track contacts that need to be synced
73
+ const contactsToAddOrUpdateLocally = [];
74
+ const contactsToDeleteLocally = [];
75
+ const contactsToUpdateRemotely = [];
76
+ // SCENARIO 2 & 6: Process remote contacts - handle new device sync and remote updates
77
+ for (const remoteContact of validRemoteContacts) {
78
+ const key = createContactKey(remoteContact);
79
+ const localContact = localContactsMap.get(key);
80
+ // Handle remote contact based on its status and local existence
81
+ if (remoteContact.deletedAt) {
82
+ // SCENARIO 8: Remote deletion - should be applied locally if contact exists locally
83
+ if (localContact) {
84
+ contactsToDeleteLocally.push(remoteContact);
96
85
  }
97
- else {
98
- // Remote is newer - use remote version
99
- contactsToAddOrUpdateLocally.push(remoteContact);
86
+ }
87
+ else if (!localContact) {
88
+ // SCENARIO 2: New contact from remote - import to local
89
+ contactsToAddOrUpdateLocally.push(remoteContact);
90
+ }
91
+ else {
92
+ // SCENARIO 4 & 6: Contact exists on both sides - check for conflicts
93
+ const hasContentDifference = localContact.name !== remoteContact.name ||
94
+ localContact.memo !== remoteContact.memo;
95
+ if (hasContentDifference) {
96
+ // Check timestamps to determine which version to keep
97
+ const localTimestamp = localContact.lastUpdatedAt || 0;
98
+ const remoteTimestamp = remoteContact.lastUpdatedAt || 0;
99
+ if (localTimestamp >= remoteTimestamp) {
100
+ // Local is newer (or same age) - use local version
101
+ contactsToUpdateRemotely.push(localContact);
102
+ }
103
+ else {
104
+ // Remote is newer - use remote version
105
+ contactsToAddOrUpdateLocally.push(remoteContact);
106
+ }
100
107
  }
108
+ // Else: content is identical, no action needed
101
109
  }
102
- // Else: content is identical, no action needed
103
110
  }
104
- }
105
- // SCENARIO 1, 3 & 5: Process local contacts not in remote - handles first sync and new local contacts
106
- for (const localContact of localVisibleContacts) {
107
- const key = createContactKey(localContact);
108
- const remoteContact = remoteContactsMap.get(key);
109
- if (!remoteContact) {
110
- // New local contact or first sync - add to remote
111
- contactsToUpdateRemotely.push(localContact);
111
+ // SCENARIO 1, 3 & 5: Process local contacts not in remote - handles first sync and new local contacts
112
+ for (const localContact of localVisibleContacts) {
113
+ const key = createContactKey(localContact);
114
+ const remoteContact = remoteContactsMap.get(key);
115
+ if (!remoteContact) {
116
+ // New local contact or first sync - add to remote
117
+ contactsToUpdateRemotely.push(localContact);
118
+ }
112
119
  }
113
- }
114
- // Apply local deletions
115
- for (const contact of contactsToDeleteLocally) {
116
- try {
120
+ // Apply local deletions
121
+ // Note: Individual errors are intentionally NOT caught here to ensure they reach Sentry
122
+ // for debugging. Previous versions silently suppressed these errors which made
123
+ // troubleshooting contact sync issues difficult.
124
+ for (const contact of contactsToDeleteLocally) {
117
125
  getMessenger().call('AddressBookController:delete', contact.chainId, contact.address);
118
126
  if (onContactDeleted) {
119
127
  onContactDeleted();
120
128
  }
121
129
  }
122
- catch (error) {
123
- console.error('Error deleting contact:', error);
124
- }
125
- }
126
- // Apply local additions/updates
127
- for (const contact of contactsToAddOrUpdateLocally) {
128
- if (!contact.deletedAt) {
129
- try {
130
+ // Apply local additions/updates
131
+ // Note: Individual errors are intentionally NOT caught here to ensure they reach Sentry
132
+ // for debugging. Previous versions silently suppressed these errors which made
133
+ // troubleshooting contact sync issues difficult.
134
+ for (const contact of contactsToAddOrUpdateLocally) {
135
+ if (!contact.deletedAt) {
130
136
  getMessenger().call('AddressBookController:set', contact.address, contact.name || '', contact.chainId, contact.memo || '', contact.addressType);
131
137
  if (onContactUpdated) {
132
138
  onContactUpdated();
133
139
  }
134
140
  }
135
- catch (error) {
136
- console.error('Error updating contact:', error);
141
+ }
142
+ // Apply changes to remote storage
143
+ if (contactsToUpdateRemotely.length > 0) {
144
+ const updatedRemoteContacts = {};
145
+ for (const localContact of contactsToUpdateRemotely) {
146
+ const key = createContactKey(localContact);
147
+ updatedRemoteContacts[key] = {
148
+ ...remoteContactsMap.get(key),
149
+ ...localContact,
150
+ lastUpdatedAt: Date.now(), // mark as updated
151
+ };
137
152
  }
153
+ // Save updated contacts to remote storage
154
+ await saveContactsToUserStorage(Object.values(updatedRemoteContacts), options);
138
155
  }
139
156
  }
140
- // Apply changes to remote storage
141
- if (contactsToUpdateRemotely.length > 0) {
142
- const updatedRemoteContacts = {};
143
- for (const localContact of contactsToUpdateRemotely) {
144
- const key = createContactKey(localContact);
145
- updatedRemoteContacts[key] = {
146
- ...remoteContactsMap.get(key),
147
- ...localContact,
148
- lastUpdatedAt: Date.now(), // mark as updated
149
- };
157
+ catch (error) {
158
+ if (onContactSyncErroneousSituation) {
159
+ onContactSyncErroneousSituation('Error synchronizing contacts', {
160
+ error,
161
+ });
162
+ // Re-throw the error to be handled by the caller
163
+ throw error;
150
164
  }
151
- // Save updated contacts to remote storage
152
- await saveContactsToUserStorage(Object.values(updatedRemoteContacts), options);
153
165
  }
154
- }
155
- catch (error) {
156
- if (onContactSyncErroneousSituation) {
157
- onContactSyncErroneousSituation('Error synchronizing contacts', {
158
- error,
159
- });
160
- // Re-throw the error to be handled by the caller
161
- throw error;
166
+ finally {
167
+ await getUserStorageControllerInstance().setIsContactSyncingInProgress(false);
162
168
  }
169
+ };
170
+ if (trace) {
171
+ // Gather pre-sync metrics for performance analysis
172
+ const initialLocalContacts = localVisibleContacts;
173
+ const initialValidRemoteContacts = validRemoteContacts;
174
+ await trace({
175
+ name: TraceName.ContactSyncFull,
176
+ data: {
177
+ localContactCount: initialLocalContacts.length,
178
+ remoteContactCount: initialValidRemoteContacts.length,
179
+ isFirstSync: initialValidRemoteContacts.length === 0 &&
180
+ initialLocalContacts.length > 0,
181
+ isNewDeviceSync: initialLocalContacts.length === 0 &&
182
+ initialValidRemoteContacts.length > 0,
183
+ isRegularSync: initialLocalContacts.length > 0 &&
184
+ initialValidRemoteContacts.length > 0,
185
+ hasDataToSync: initialLocalContacts.length > 0 ||
186
+ initialValidRemoteContacts.length > 0,
187
+ expectedWorkload: initialLocalContacts.length + initialValidRemoteContacts.length,
188
+ },
189
+ }, performSync);
190
+ return;
163
191
  }
164
- finally {
165
- await getUserStorageControllerInstance().setIsContactSyncingInProgress(false);
166
- }
192
+ await performSync();
167
193
  }
168
194
  /**
169
195
  * Retrieves remote contacts from user storage API
@@ -194,19 +220,34 @@ async function getRemoteContacts(options) {
194
220
  *
195
221
  * @param contacts - The contacts to save to user storage
196
222
  * @param options - Parameters used for saving contacts
223
+ * @returns Promise that resolves when contacts are saved
197
224
  */
198
225
  async function saveContactsToUserStorage(contacts, options) {
199
- const { getUserStorageControllerInstance } = options;
200
- if (!contacts || contacts.length === 0) {
201
- return;
202
- }
203
- // Convert each AddressBookEntry to UserStorageContactEntry format and create key-value pairs
204
- const storageEntries = contacts.map((contact) => {
205
- const key = createContactKey(contact);
206
- const storageEntry = mapAddressBookEntryToUserStorageEntry(contact);
207
- return [key, JSON.stringify(storageEntry)];
208
- });
209
- await getUserStorageControllerInstance().performBatchSetStorage(USER_STORAGE_FEATURE_NAMES.addressBook, storageEntries);
226
+ const { getUserStorageControllerInstance, trace } = options;
227
+ const saveContacts = async () => {
228
+ if (!contacts || contacts.length === 0) {
229
+ return;
230
+ }
231
+ // Convert each AddressBookEntry to UserStorageContactEntry format and create key-value pairs
232
+ const storageEntries = contacts.map((contact) => {
233
+ const key = createContactKey(contact);
234
+ const storageEntry = mapAddressBookEntryToUserStorageEntry(contact);
235
+ return [key, JSON.stringify(storageEntry)];
236
+ });
237
+ await getUserStorageControllerInstance().performBatchSetStorage(USER_STORAGE_FEATURE_NAMES.addressBook, storageEntries);
238
+ };
239
+ return trace
240
+ ? await trace({
241
+ name: TraceName.ContactSyncSaveBatch,
242
+ data: {
243
+ contactCount: contacts.length,
244
+ // Performance scaling indicators
245
+ hasBatchOperations: contacts.length > 1,
246
+ chainCount: new Set(contacts.map((c) => c.chainId)).size,
247
+ hasMemosCount: contacts.filter((c) => c.memo?.length).length,
248
+ },
249
+ }, saveContacts)
250
+ : await saveContacts();
210
251
  }
211
252
  /**
212
253
  * Updates a single contact in remote storage without performing a full sync
@@ -214,24 +255,41 @@ async function saveContactsToUserStorage(contacts, options) {
214
255
  *
215
256
  * @param contact - The contact that was updated locally
216
257
  * @param options - Parameters used for syncing operations
258
+ * @returns Promise that resolves when the contact is updated
217
259
  */
218
260
  export async function updateContactInRemoteStorage(contact, options) {
219
- if (!canPerformContactSyncing(options) ||
220
- !contact.address ||
221
- !contact.chainId ||
222
- !contact.name?.trim()) {
223
- return;
224
- }
225
- const { getUserStorageControllerInstance } = options;
226
- // Create an updated entry with timestamp
227
- const updatedEntry = {
228
- ...contact,
229
- lastUpdatedAt: contact.lastUpdatedAt || Date.now(),
261
+ const { trace } = options;
262
+ const updateContact = async () => {
263
+ if (!canPerformContactSyncing(options) ||
264
+ !contact.address ||
265
+ !contact.chainId ||
266
+ !contact.name?.trim()) {
267
+ return;
268
+ }
269
+ const { getUserStorageControllerInstance } = options;
270
+ // Create an updated entry with timestamp
271
+ const updatedEntry = {
272
+ ...contact,
273
+ lastUpdatedAt: contact.lastUpdatedAt || Date.now(),
274
+ };
275
+ const key = createContactKey(contact);
276
+ const storageEntry = mapAddressBookEntryToUserStorageEntry(updatedEntry);
277
+ // Save individual contact to remote storage
278
+ await getUserStorageControllerInstance().performSetStorage(`${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`, JSON.stringify(storageEntry));
230
279
  };
231
- const key = createContactKey(contact);
232
- const storageEntry = mapAddressBookEntryToUserStorageEntry(updatedEntry);
233
- // Save individual contact to remote storage
234
- await getUserStorageControllerInstance().performSetStorage(`${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`, JSON.stringify(storageEntry));
280
+ if (trace) {
281
+ return await trace({
282
+ name: TraceName.ContactSyncUpdateRemote,
283
+ data: {
284
+ chainId: contact.chainId,
285
+ // Performance indicators
286
+ hasTimestamp: Boolean(contact.lastUpdatedAt),
287
+ hasMemo: Boolean(contact.memo?.length),
288
+ isUpdate: Boolean(contact.lastUpdatedAt), // vs new contact
289
+ },
290
+ }, updateContact);
291
+ }
292
+ return await updateContact();
235
293
  }
236
294
  /**
237
295
  * Marks a single contact as deleted in remote storage without performing a full sync
@@ -239,37 +297,44 @@ export async function updateContactInRemoteStorage(contact, options) {
239
297
  *
240
298
  * @param contact - The contact that was deleted locally (contains at least address and chainId)
241
299
  * @param options - Parameters used for syncing operations
300
+ * @returns Promise that resolves when the contact is marked as deleted
242
301
  */
243
302
  export async function deleteContactInRemoteStorage(contact, options) {
244
- if (!canPerformContactSyncing(options) ||
245
- !contact.address ||
246
- !contact.chainId ||
247
- !contact.name?.trim()) {
248
- return;
249
- }
250
- const { getUserStorageControllerInstance } = options;
251
- const key = createContactKey(contact);
252
- try {
253
- // Try to get the existing contact first
254
- const existingContactJson = await getUserStorageControllerInstance().performGetStorage(`${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`);
255
- if (existingContactJson) {
256
- // Mark the existing contact as deleted
257
- const existingStorageEntry = JSON.parse(existingContactJson);
258
- const existingContact = mapUserStorageEntryToAddressBookEntry(existingStorageEntry);
259
- const now = Date.now();
260
- const deletedContact = {
261
- ...existingContact,
262
- deletedAt: now,
263
- lastUpdatedAt: now,
264
- };
265
- const deletedStorageEntry = mapAddressBookEntryToUserStorageEntry(deletedContact);
266
- // Save the deleted contact back to storage
267
- await getUserStorageControllerInstance().performSetStorage(`${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`, JSON.stringify(deletedStorageEntry));
303
+ const { trace } = options;
304
+ const deleteContact = async () => {
305
+ if (!canPerformContactSyncing(options) ||
306
+ !contact.address ||
307
+ !contact.chainId ||
308
+ !contact.name?.trim()) {
309
+ return;
268
310
  }
269
- }
270
- catch {
271
- // If contact doesn't exist in remote storage, no need to mark as deleted
272
- console.warn('Contact not found in remote storage for deletion:', key);
273
- }
311
+ const { getUserStorageControllerInstance } = options;
312
+ const key = createContactKey(contact);
313
+ try {
314
+ // Try to get the existing contact first
315
+ const existingContactJson = await getUserStorageControllerInstance().performGetStorage(`${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`);
316
+ if (existingContactJson) {
317
+ // Mark the existing contact as deleted
318
+ const existingStorageEntry = JSON.parse(existingContactJson);
319
+ const existingContact = mapUserStorageEntryToAddressBookEntry(existingStorageEntry);
320
+ const now = Date.now();
321
+ const deletedContact = {
322
+ ...existingContact,
323
+ deletedAt: now,
324
+ lastUpdatedAt: now,
325
+ };
326
+ const deletedStorageEntry = mapAddressBookEntryToUserStorageEntry(deletedContact);
327
+ // Save the deleted contact back to storage
328
+ await getUserStorageControllerInstance().performSetStorage(`${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`, JSON.stringify(deletedStorageEntry));
329
+ }
330
+ }
331
+ catch {
332
+ // If contact doesn't exist in remote storage, no need to mark as deleted
333
+ console.warn('Contact not found in remote storage for deletion:', key);
334
+ }
335
+ };
336
+ return trace
337
+ ? await trace({ name: TraceName.ContactSyncDeleteRemote }, deleteContact)
338
+ : await deleteContact();
274
339
  }
275
340
  //# sourceMappingURL=controller-integration.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"controller-integration.mjs","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/controller-integration.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AddressBookEntry } from '@metamask/address-book-controller';\n\nimport { canPerformContactSyncing } from './sync-utils';\nimport type { ContactSyncingOptions } from './types';\nimport type { UserStorageContactEntry } from './types';\nimport {\n mapAddressBookEntryToUserStorageEntry,\n mapUserStorageEntryToAddressBookEntry,\n type SyncAddressBookEntry,\n} from './utils';\nimport { isContactBridgedFromAccounts } from './utils';\nimport { USER_STORAGE_FEATURE_NAMES } from '../../../shared/storage-schema';\n\nexport type SyncContactsWithUserStorageConfig = {\n onContactSyncErroneousSituation?: (\n errorMessage: string,\n sentryContext?: Record<string, unknown>,\n ) => void;\n onContactUpdated?: () => void;\n onContactDeleted?: () => void;\n};\n\n/**\n * Creates a unique key for a contact based on chainId and address\n *\n * @param contact - The contact to create a key for\n * @returns A unique string key\n */\nfunction createContactKey(contact: AddressBookEntry): string {\n if (!contact.address) {\n throw new Error('Contact address is required to create storage key');\n }\n return `${contact.chainId}_${contact.address.toLowerCase()}`;\n}\n\n/**\n * Syncs contacts between local storage and user storage (remote).\n *\n * Handles the following syncing scenarios:\n * 1. First Sync: When local contacts exist but there are no remote contacts, uploads all local contacts.\n * 2. New Device Sync: Downloads remote contacts that don't exist locally (empty local address book).\n * 3. Simple Merge: Ensures both sides (local & remote) have all contacts.\n * 4. Contact Naming Conflicts: When same contact has different names, uses most recent by timestamp.\n * 5. Local Updates: When a contact was updated locally, syncs changes to remote if local is newer.\n * 6. Remote Updates: When a contact was updated remotely, applies changes locally if remote is newer.\n * 7. Local Deletions: Handled by real-time event handlers (deleteContactInRemoteStorage) to prevent false positives.\n * 8. Remote Deletions: When a contact was deleted remotely, applies deletion locally.\n * 9. Concurrent Updates: Resolves conflicts using timestamps to determine the winner.\n * 10. Restore After Delete: If a contact is modified after being deleted, restores it.\n * 11. ChainId Differences: Treats same address on different chains as separate contacts.\n *\n * @param config - Parameters used for syncing callbacks\n * @param options - Parameters used for syncing operations\n */\nexport async function syncContactsWithUserStorage(\n config: SyncContactsWithUserStorageConfig,\n options: ContactSyncingOptions,\n): Promise<void> {\n const { getMessenger, getUserStorageControllerInstance } = options;\n const {\n onContactSyncErroneousSituation,\n onContactUpdated,\n onContactDeleted,\n } = config;\n\n try {\n // Cannot perform sync, conditions not met\n if (!canPerformContactSyncing(options)) {\n return;\n }\n\n // Activate sync semaphore to prevent event loops\n await getUserStorageControllerInstance().setIsContactSyncingInProgress(\n true,\n );\n\n // Get all local contacts from AddressBookController (exclude chain \"*\" contacts)\n const localVisibleContacts =\n getMessenger()\n .call('AddressBookController:list')\n .filter((contact) => !isContactBridgedFromAccounts(contact))\n .filter(\n (contact) =>\n contact.address && contact.chainId && contact.name?.trim(),\n ) || [];\n\n // Get remote contacts from user storage API\n const remoteContacts = await getRemoteContacts(options);\n\n // Filter remote contacts to exclude invalid ones (or empty array if no remote contacts)\n const validRemoteContacts =\n remoteContacts?.filter(\n (contact) => contact.address && contact.chainId && contact.name?.trim(),\n ) || [];\n\n // Prepare maps for efficient lookup\n const localContactsMap = new Map<string, AddressBookEntry>();\n const remoteContactsMap = new Map<string, SyncAddressBookEntry>();\n\n localVisibleContacts.forEach((contact) => {\n const key = createContactKey(contact);\n localContactsMap.set(key, contact);\n });\n\n validRemoteContacts.forEach((contact) => {\n const key = createContactKey(contact);\n remoteContactsMap.set(key, contact);\n });\n\n // Lists to track contacts that need to be synced\n const contactsToAddOrUpdateLocally: SyncAddressBookEntry[] = [];\n const contactsToDeleteLocally: SyncAddressBookEntry[] = [];\n const contactsToUpdateRemotely: AddressBookEntry[] = [];\n\n // SCENARIO 2 & 6: Process remote contacts - handle new device sync and remote updates\n for (const remoteContact of validRemoteContacts) {\n const key = createContactKey(remoteContact);\n const localContact = localContactsMap.get(key);\n\n // Handle remote contact based on its status and local existence\n if (remoteContact.deletedAt) {\n // SCENARIO 8: Remote deletion - should be applied locally if contact exists locally\n if (localContact) {\n contactsToDeleteLocally.push(remoteContact);\n }\n } else if (!localContact) {\n // SCENARIO 2: New contact from remote - import to local\n contactsToAddOrUpdateLocally.push(remoteContact);\n } else {\n // SCENARIO 4 & 6: Contact exists on both sides - check for conflicts\n const hasContentDifference =\n localContact.name !== remoteContact.name ||\n localContact.memo !== remoteContact.memo;\n\n if (hasContentDifference) {\n // Check timestamps to determine which version to keep\n const localTimestamp = localContact.lastUpdatedAt || 0;\n const remoteTimestamp = remoteContact.lastUpdatedAt || 0;\n\n if (localTimestamp >= remoteTimestamp) {\n // Local is newer (or same age) - use local version\n contactsToUpdateRemotely.push(localContact);\n } else {\n // Remote is newer - use remote version\n contactsToAddOrUpdateLocally.push(remoteContact);\n }\n }\n\n // Else: content is identical, no action needed\n }\n }\n\n // SCENARIO 1, 3 & 5: Process local contacts not in remote - handles first sync and new local contacts\n for (const localContact of localVisibleContacts) {\n const key = createContactKey(localContact);\n const remoteContact = remoteContactsMap.get(key);\n\n if (!remoteContact) {\n // New local contact or first sync - add to remote\n contactsToUpdateRemotely.push(localContact);\n }\n }\n\n // Apply local deletions\n for (const contact of contactsToDeleteLocally) {\n try {\n getMessenger().call(\n 'AddressBookController:delete',\n contact.chainId,\n contact.address,\n );\n\n if (onContactDeleted) {\n onContactDeleted();\n }\n } catch (error) {\n console.error('Error deleting contact:', error);\n }\n }\n\n // Apply local additions/updates\n for (const contact of contactsToAddOrUpdateLocally) {\n if (!contact.deletedAt) {\n try {\n getMessenger().call(\n 'AddressBookController:set',\n contact.address,\n contact.name || '',\n contact.chainId,\n contact.memo || '',\n contact.addressType,\n );\n\n if (onContactUpdated) {\n onContactUpdated();\n }\n } catch (error) {\n console.error('Error updating contact:', error);\n }\n }\n }\n\n // Apply changes to remote storage\n if (contactsToUpdateRemotely.length > 0) {\n const updatedRemoteContacts: Record<string, SyncAddressBookEntry> = {};\n for (const localContact of contactsToUpdateRemotely) {\n const key = createContactKey(localContact);\n updatedRemoteContacts[key] = {\n ...remoteContactsMap.get(key), // Start with an existing remote contact if it exists\n ...localContact, // override with local changes\n lastUpdatedAt: Date.now(), // mark as updated\n };\n }\n // Save updated contacts to remote storage\n await saveContactsToUserStorage(\n Object.values(updatedRemoteContacts),\n options,\n );\n }\n } catch (error) {\n if (onContactSyncErroneousSituation) {\n onContactSyncErroneousSituation('Error synchronizing contacts', {\n error,\n });\n\n // Re-throw the error to be handled by the caller\n throw error;\n }\n } finally {\n await getUserStorageControllerInstance().setIsContactSyncingInProgress(\n false,\n );\n }\n}\n\n/**\n * Retrieves remote contacts from user storage API\n *\n * @param options - Parameters used for retrieving remote contacts\n * @returns Array of contacts from remote storage, or null if none found\n */\nasync function getRemoteContacts(\n options: ContactSyncingOptions,\n): Promise<SyncAddressBookEntry[] | null> {\n const { getUserStorageControllerInstance } = options;\n\n try {\n const remoteContactsJsonArray =\n await getUserStorageControllerInstance().performGetStorageAllFeatureEntries(\n USER_STORAGE_FEATURE_NAMES.addressBook,\n );\n\n if (!remoteContactsJsonArray || remoteContactsJsonArray.length === 0) {\n return null;\n }\n\n // Parse each JSON entry and convert from UserStorageContactEntry to AddressBookEntry\n const remoteStorageEntries = remoteContactsJsonArray.map((contactJson) => {\n const entry = JSON.parse(contactJson) as UserStorageContactEntry;\n return mapUserStorageEntryToAddressBookEntry(entry);\n });\n\n return remoteStorageEntries;\n } catch {\n return null;\n }\n}\n\n/**\n * Saves local contacts to user storage\n *\n * @param contacts - The contacts to save to user storage\n * @param options - Parameters used for saving contacts\n */\nasync function saveContactsToUserStorage(\n contacts: AddressBookEntry[],\n options: ContactSyncingOptions,\n): Promise<void> {\n const { getUserStorageControllerInstance } = options;\n\n if (!contacts || contacts.length === 0) {\n return;\n }\n\n // Convert each AddressBookEntry to UserStorageContactEntry format and create key-value pairs\n const storageEntries: [string, string][] = contacts.map((contact) => {\n const key = createContactKey(contact);\n const storageEntry = mapAddressBookEntryToUserStorageEntry(contact);\n return [key, JSON.stringify(storageEntry)];\n });\n\n await getUserStorageControllerInstance().performBatchSetStorage(\n USER_STORAGE_FEATURE_NAMES.addressBook,\n storageEntries,\n );\n}\n\n/**\n * Updates a single contact in remote storage without performing a full sync\n * This is used when a contact is updated locally to efficiently push changes to remote\n *\n * @param contact - The contact that was updated locally\n * @param options - Parameters used for syncing operations\n */\nexport async function updateContactInRemoteStorage(\n contact: AddressBookEntry,\n options: ContactSyncingOptions,\n): Promise<void> {\n if (\n !canPerformContactSyncing(options) ||\n !contact.address ||\n !contact.chainId ||\n !contact.name?.trim()\n ) {\n return;\n }\n\n const { getUserStorageControllerInstance } = options;\n\n // Create an updated entry with timestamp\n const updatedEntry = {\n ...contact,\n lastUpdatedAt: contact.lastUpdatedAt || Date.now(),\n } as SyncAddressBookEntry;\n\n const key = createContactKey(contact);\n const storageEntry = mapAddressBookEntryToUserStorageEntry(updatedEntry);\n\n // Save individual contact to remote storage\n await getUserStorageControllerInstance().performSetStorage(\n `${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`,\n JSON.stringify(storageEntry),\n );\n}\n\n/**\n * Marks a single contact as deleted in remote storage without performing a full sync\n * This is used when a contact is deleted locally to efficiently push the deletion to remote\n *\n * @param contact - The contact that was deleted locally (contains at least address and chainId)\n * @param options - Parameters used for syncing operations\n */\nexport async function deleteContactInRemoteStorage(\n contact: AddressBookEntry,\n options: ContactSyncingOptions,\n): Promise<void> {\n if (\n !canPerformContactSyncing(options) ||\n !contact.address ||\n !contact.chainId ||\n !contact.name?.trim()\n ) {\n return;\n }\n\n const { getUserStorageControllerInstance } = options;\n const key = createContactKey(contact);\n\n try {\n // Try to get the existing contact first\n const existingContactJson =\n await getUserStorageControllerInstance().performGetStorage(\n `${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`,\n );\n\n if (existingContactJson) {\n // Mark the existing contact as deleted\n const existingStorageEntry = JSON.parse(\n existingContactJson,\n ) as UserStorageContactEntry;\n const existingContact =\n mapUserStorageEntryToAddressBookEntry(existingStorageEntry);\n\n const now = Date.now();\n const deletedContact = {\n ...existingContact,\n deletedAt: now,\n lastUpdatedAt: now,\n } as SyncAddressBookEntry;\n\n const deletedStorageEntry =\n mapAddressBookEntryToUserStorageEntry(deletedContact);\n\n // Save the deleted contact back to storage\n await getUserStorageControllerInstance().performSetStorage(\n `${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`,\n JSON.stringify(deletedStorageEntry),\n );\n }\n } catch {\n // If contact doesn't exist in remote storage, no need to mark as deleted\n console.warn('Contact not found in remote storage for deletion:', key);\n }\n}\n"]}
1
+ {"version":3,"file":"controller-integration.mjs","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/controller-integration.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,yBAAqB;AAGxD,OAAO,EACL,qCAAqC,EACrC,qCAAqC,EAEtC,oBAAgB;AACjB,OAAO,EAAE,4BAA4B,EAAE,oBAAgB;AACvD,OAAO,EAAE,0BAA0B,EAAE,2CAAuC;AAC5E,OAAO,EAAE,SAAS,EAAE,yBAAqB;AAWzC;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,OAAyB;IACjD,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;QACpB,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;KACtE;IACD,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC;AAC/D,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,KAAK,UAAU,2BAA2B,CAC/C,MAAyC,EACzC,OAA8B;IAE9B,MAAM,EAAE,YAAY,EAAE,gCAAgC,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1E,MAAM,EACJ,+BAA+B,EAC/B,gBAAgB,EAChB,gBAAgB,GACjB,GAAG,MAAM,CAAC;IAEX,0CAA0C;IAC1C,IAAI,CAAC,wBAAwB,CAAC,OAAO,CAAC,EAAE;QACtC,OAAO;KACR;IAED,sGAAsG;IACtG,8FAA8F;IAC9F,+EAA+E;IAE/E,iFAAiF;IACjF,MAAM,oBAAoB,GACxB,YAAY,EAAE;SACX,IAAI,CAAC,4BAA4B,CAAC;SAClC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,4BAA4B,CAAC,OAAO,CAAC,CAAC;SAC3D,MAAM,CACL,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,CACxE,IAAI,EAAE,CAAC;IAEZ,4CAA4C;IAC5C,MAAM,cAAc,GAAG,MAAM,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAExD,wFAAwF;IACxF,MAAM,mBAAmB,GACvB,cAAc,EAAE,MAAM,CACpB,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,CACxE,IAAI,EAAE,CAAC;IAEV,MAAM,WAAW,GAAG,KAAK,IAAI,EAAE;QAC7B,IAAI;YACF,iDAAiD;YACjD,MAAM,gCAAgC,EAAE,CAAC,6BAA6B,CACpE,IAAI,CACL,CAAC;YAEF,oCAAoC;YACpC,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAA4B,CAAC;YAC7D,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAgC,CAAC;YAElE,oBAAoB,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBACvC,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;gBACtC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACrC,CAAC,CAAC,CAAC;YAEH,mBAAmB,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;gBACtC,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;gBACtC,iBAAiB,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YACtC,CAAC,CAAC,CAAC;YAEH,iDAAiD;YACjD,MAAM,4BAA4B,GAA2B,EAAE,CAAC;YAChE,MAAM,uBAAuB,GAA2B,EAAE,CAAC;YAC3D,MAAM,wBAAwB,GAAuB,EAAE,CAAC;YAExD,sFAAsF;YACtF,KAAK,MAAM,aAAa,IAAI,mBAAmB,EAAE;gBAC/C,MAAM,GAAG,GAAG,gBAAgB,CAAC,aAAa,CAAC,CAAC;gBAC5C,MAAM,YAAY,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAE/C,gEAAgE;gBAChE,IAAI,aAAa,CAAC,SAAS,EAAE;oBAC3B,oFAAoF;oBACpF,IAAI,YAAY,EAAE;wBAChB,uBAAuB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;qBAC7C;iBACF;qBAAM,IAAI,CAAC,YAAY,EAAE;oBACxB,wDAAwD;oBACxD,4BAA4B,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;iBAClD;qBAAM;oBACL,qEAAqE;oBACrE,MAAM,oBAAoB,GACxB,YAAY,CAAC,IAAI,KAAK,aAAa,CAAC,IAAI;wBACxC,YAAY,CAAC,IAAI,KAAK,aAAa,CAAC,IAAI,CAAC;oBAE3C,IAAI,oBAAoB,EAAE;wBACxB,sDAAsD;wBACtD,MAAM,cAAc,GAAG,YAAY,CAAC,aAAa,IAAI,CAAC,CAAC;wBACvD,MAAM,eAAe,GAAG,aAAa,CAAC,aAAa,IAAI,CAAC,CAAC;wBAEzD,IAAI,cAAc,IAAI,eAAe,EAAE;4BACrC,mDAAmD;4BACnD,wBAAwB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;yBAC7C;6BAAM;4BACL,uCAAuC;4BACvC,4BAA4B,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;yBAClD;qBACF;oBAED,+CAA+C;iBAChD;aACF;YAED,sGAAsG;YACtG,KAAK,MAAM,YAAY,IAAI,oBAAoB,EAAE;gBAC/C,MAAM,GAAG,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC;gBAC3C,MAAM,aAAa,GAAG,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAEjD,IAAI,CAAC,aAAa,EAAE;oBAClB,kDAAkD;oBAClD,wBAAwB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;iBAC7C;aACF;YAED,wBAAwB;YACxB,wFAAwF;YACxF,+EAA+E;YAC/E,iDAAiD;YACjD,KAAK,MAAM,OAAO,IAAI,uBAAuB,EAAE;gBAC7C,YAAY,EAAE,CAAC,IAAI,CACjB,8BAA8B,EAC9B,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,OAAO,CAChB,CAAC;gBAEF,IAAI,gBAAgB,EAAE;oBACpB,gBAAgB,EAAE,CAAC;iBACpB;aACF;YAED,gCAAgC;YAChC,wFAAwF;YACxF,+EAA+E;YAC/E,iDAAiD;YACjD,KAAK,MAAM,OAAO,IAAI,4BAA4B,EAAE;gBAClD,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE;oBACtB,YAAY,EAAE,CAAC,IAAI,CACjB,2BAA2B,EAC3B,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,IAAI,IAAI,EAAE,EAClB,OAAO,CAAC,OAAO,EACf,OAAO,CAAC,IAAI,IAAI,EAAE,EAClB,OAAO,CAAC,WAAW,CACpB,CAAC;oBAEF,IAAI,gBAAgB,EAAE;wBACpB,gBAAgB,EAAE,CAAC;qBACpB;iBACF;aACF;YAED,kCAAkC;YAClC,IAAI,wBAAwB,CAAC,MAAM,GAAG,CAAC,EAAE;gBACvC,MAAM,qBAAqB,GAAyC,EAAE,CAAC;gBACvE,KAAK,MAAM,YAAY,IAAI,wBAAwB,EAAE;oBACnD,MAAM,GAAG,GAAG,gBAAgB,CAAC,YAAY,CAAC,CAAC;oBAC3C,qBAAqB,CAAC,GAAG,CAAC,GAAG;wBAC3B,GAAG,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC;wBAC7B,GAAG,YAAY;wBACf,aAAa,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,kBAAkB;qBAC9C,CAAC;iBACH;gBACD,0CAA0C;gBAC1C,MAAM,yBAAyB,CAC7B,MAAM,CAAC,MAAM,CAAC,qBAAqB,CAAC,EACpC,OAAO,CACR,CAAC;aACH;SACF;QAAC,OAAO,KAAK,EAAE;YACd,IAAI,+BAA+B,EAAE;gBACnC,+BAA+B,CAAC,8BAA8B,EAAE;oBAC9D,KAAK;iBACN,CAAC,CAAC;gBAEH,iDAAiD;gBACjD,MAAM,KAAK,CAAC;aACb;SACF;gBAAS;YACR,MAAM,gCAAgC,EAAE,CAAC,6BAA6B,CACpE,KAAK,CACN,CAAC;SACH;IACH,CAAC,CAAC;IAEF,IAAI,KAAK,EAAE;QACT,mDAAmD;QACnD,MAAM,oBAAoB,GAAG,oBAAoB,CAAC;QAClD,MAAM,0BAA0B,GAAG,mBAAmB,CAAC;QAEvD,MAAM,KAAK,CACT;YACE,IAAI,EAAE,SAAS,CAAC,eAAe;YAC/B,IAAI,EAAE;gBACJ,iBAAiB,EAAE,oBAAoB,CAAC,MAAM;gBAC9C,kBAAkB,EAAE,0BAA0B,CAAC,MAAM;gBACrD,WAAW,EACT,0BAA0B,CAAC,MAAM,KAAK,CAAC;oBACvC,oBAAoB,CAAC,MAAM,GAAG,CAAC;gBACjC,eAAe,EACb,oBAAoB,CAAC,MAAM,KAAK,CAAC;oBACjC,0BAA0B,CAAC,MAAM,GAAG,CAAC;gBACvC,aAAa,EACX,oBAAoB,CAAC,MAAM,GAAG,CAAC;oBAC/B,0BAA0B,CAAC,MAAM,GAAG,CAAC;gBACvC,aAAa,EACX,oBAAoB,CAAC,MAAM,GAAG,CAAC;oBAC/B,0BAA0B,CAAC,MAAM,GAAG,CAAC;gBACvC,gBAAgB,EACd,oBAAoB,CAAC,MAAM,GAAG,0BAA0B,CAAC,MAAM;aAClE;SACF,EACD,WAAW,CACZ,CAAC;QAEF,OAAO;KACR;IAED,MAAM,WAAW,EAAE,CAAC;AACtB,CAAC;AAED;;;;;GAKG;AACH,KAAK,UAAU,iBAAiB,CAC9B,OAA8B;IAE9B,MAAM,EAAE,gCAAgC,EAAE,GAAG,OAAO,CAAC;IAErD,IAAI;QACF,MAAM,uBAAuB,GAC3B,MAAM,gCAAgC,EAAE,CAAC,kCAAkC,CACzE,0BAA0B,CAAC,WAAW,CACvC,CAAC;QAEJ,IAAI,CAAC,uBAAuB,IAAI,uBAAuB,CAAC,MAAM,KAAK,CAAC,EAAE;YACpE,OAAO,IAAI,CAAC;SACb;QAED,qFAAqF;QACrF,MAAM,oBAAoB,GAAG,uBAAuB,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE;YACvE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAA4B,CAAC;YACjE,OAAO,qCAAqC,CAAC,KAAK,CAAC,CAAC;QACtD,CAAC,CAAC,CAAC;QAEH,OAAO,oBAAoB,CAAC;KAC7B;IAAC,MAAM;QACN,OAAO,IAAI,CAAC;KACb;AACH,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,yBAAyB,CACtC,QAA4B,EAC5B,OAA8B;IAE9B,MAAM,EAAE,gCAAgC,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAE5D,MAAM,YAAY,GAAG,KAAK,IAAI,EAAE;QAC9B,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE;YACtC,OAAO;SACR;QAED,6FAA6F;QAC7F,MAAM,cAAc,GAAuB,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE;YAClE,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,YAAY,GAAG,qCAAqC,CAAC,OAAO,CAAC,CAAC;YACpE,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QAEH,MAAM,gCAAgC,EAAE,CAAC,sBAAsB,CAC7D,0BAA0B,CAAC,WAAW,EACtC,cAAc,CACf,CAAC;IACJ,CAAC,CAAC;IAEF,OAAO,KAAK;QACV,CAAC,CAAC,MAAM,KAAK,CACT;YACE,IAAI,EAAE,SAAS,CAAC,oBAAoB;YACpC,IAAI,EAAE;gBACJ,YAAY,EAAE,QAAQ,CAAC,MAAM;gBAC7B,iCAAiC;gBACjC,kBAAkB,EAAE,QAAQ,CAAC,MAAM,GAAG,CAAC;gBACvC,UAAU,EAAE,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;gBACxD,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM;aAC7D;SACF,EACD,YAAY,CACb;QACH,CAAC,CAAC,MAAM,YAAY,EAAE,CAAC;AAC3B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,OAAyB,EACzB,OAA8B;IAE9B,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAE1B,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,IACE,CAAC,wBAAwB,CAAC,OAAO,CAAC;YAClC,CAAC,OAAO,CAAC,OAAO;YAChB,CAAC,OAAO,CAAC,OAAO;YAChB,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,EACrB;YACA,OAAO;SACR;QAED,MAAM,EAAE,gCAAgC,EAAE,GAAG,OAAO,CAAC;QAErD,yCAAyC;QACzC,MAAM,YAAY,GAAG;YACnB,GAAG,OAAO;YACV,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,IAAI,CAAC,GAAG,EAAE;SAC3B,CAAC;QAE1B,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,YAAY,GAAG,qCAAqC,CAAC,YAAY,CAAC,CAAC;QAEzE,4CAA4C;QAC5C,MAAM,gCAAgC,EAAE,CAAC,iBAAiB,CACxD,GAAG,0BAA0B,CAAC,WAAW,IAAI,GAAG,EAAE,EAClD,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,CAC7B,CAAC;IACJ,CAAC,CAAC;IAEF,IAAI,KAAK,EAAE;QACT,OAAO,MAAM,KAAK,CAChB;YACE,IAAI,EAAE,SAAS,CAAC,uBAAuB;YACvC,IAAI,EAAE;gBACJ,OAAO,EAAE,OAAO,CAAC,OAAO;gBACxB,yBAAyB;gBACzB,YAAY,EAAE,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC;gBAC5C,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;gBACtC,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,iBAAiB;aAC5D;SACF,EACD,aAAa,CACd,CAAC;KACH;IAED,OAAO,MAAM,aAAa,EAAE,CAAC;AAC/B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,OAAyB,EACzB,OAA8B;IAE9B,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1B,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,IACE,CAAC,wBAAwB,CAAC,OAAO,CAAC;YAClC,CAAC,OAAO,CAAC,OAAO;YAChB,CAAC,OAAO,CAAC,OAAO;YAChB,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,EACrB;YACA,OAAO;SACR;QAED,MAAM,EAAE,gCAAgC,EAAE,GAAG,OAAO,CAAC;QACrD,MAAM,GAAG,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAEtC,IAAI;YACF,wCAAwC;YACxC,MAAM,mBAAmB,GACvB,MAAM,gCAAgC,EAAE,CAAC,iBAAiB,CACxD,GAAG,0BAA0B,CAAC,WAAW,IAAI,GAAG,EAAE,CACnD,CAAC;YAEJ,IAAI,mBAAmB,EAAE;gBACvB,uCAAuC;gBACvC,MAAM,oBAAoB,GAAG,IAAI,CAAC,KAAK,CACrC,mBAAmB,CACO,CAAC;gBAC7B,MAAM,eAAe,GACnB,qCAAqC,CAAC,oBAAoB,CAAC,CAAC;gBAE9D,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBACvB,MAAM,cAAc,GAAG;oBACrB,GAAG,eAAe;oBAClB,SAAS,EAAE,GAAG;oBACd,aAAa,EAAE,GAAG;iBACK,CAAC;gBAE1B,MAAM,mBAAmB,GACvB,qCAAqC,CAAC,cAAc,CAAC,CAAC;gBAExD,2CAA2C;gBAC3C,MAAM,gCAAgC,EAAE,CAAC,iBAAiB,CACxD,GAAG,0BAA0B,CAAC,WAAW,IAAI,GAAG,EAAE,EAClD,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC,CACpC,CAAC;aACH;SACF;QAAC,MAAM;YACN,yEAAyE;YACzE,OAAO,CAAC,IAAI,CAAC,mDAAmD,EAAE,GAAG,CAAC,CAAC;SACxE;IACH,CAAC,CAAC;IAEF,OAAO,KAAK;QACV,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,uBAAuB,EAAE,EAAE,aAAa,CAAC;QACzE,CAAC,CAAC,MAAM,aAAa,EAAE,CAAC;AAC5B,CAAC","sourcesContent":["import type { AddressBookEntry } from '@metamask/address-book-controller';\n\nimport { canPerformContactSyncing } from './sync-utils';\nimport type { ContactSyncingOptions } from './types';\nimport type { UserStorageContactEntry } from './types';\nimport {\n mapAddressBookEntryToUserStorageEntry,\n mapUserStorageEntryToAddressBookEntry,\n type SyncAddressBookEntry,\n} from './utils';\nimport { isContactBridgedFromAccounts } from './utils';\nimport { USER_STORAGE_FEATURE_NAMES } from '../../../shared/storage-schema';\nimport { TraceName } from '../constants';\n\nexport type SyncContactsWithUserStorageConfig = {\n onContactSyncErroneousSituation?: (\n errorMessage: string,\n sentryContext?: Record<string, unknown>,\n ) => void;\n onContactUpdated?: () => void;\n onContactDeleted?: () => void;\n};\n\n/**\n * Creates a unique key for a contact based on chainId and address\n *\n * @param contact - The contact to create a key for\n * @returns A unique string key\n */\nfunction createContactKey(contact: AddressBookEntry): string {\n if (!contact.address) {\n throw new Error('Contact address is required to create storage key');\n }\n return `${contact.chainId}_${contact.address.toLowerCase()}`;\n}\n\n/**\n * Syncs contacts between local storage and user storage (remote).\n *\n * Handles the following syncing scenarios:\n * 1. First Sync: When local contacts exist but there are no remote contacts, uploads all local contacts.\n * 2. New Device Sync: Downloads remote contacts that don't exist locally (empty local address book).\n * 3. Simple Merge: Ensures both sides (local & remote) have all contacts.\n * 4. Contact Naming Conflicts: When same contact has different names, uses most recent by timestamp.\n * 5. Local Updates: When a contact was updated locally, syncs changes to remote if local is newer.\n * 6. Remote Updates: When a contact was updated remotely, applies changes locally if remote is newer.\n * 7. Local Deletions: Handled by real-time event handlers (deleteContactInRemoteStorage) to prevent false positives.\n * 8. Remote Deletions: When a contact was deleted remotely, applies deletion locally.\n * 9. Concurrent Updates: Resolves conflicts using timestamps to determine the winner.\n * 10. Restore After Delete: If a contact is modified after being deleted, restores it.\n * 11. ChainId Differences: Treats same address on different chains as separate contacts.\n *\n * @param config - Parameters used for syncing callbacks\n * @param options - Parameters used for syncing operations\n * @returns Promise that resolves when contact synchronization is complete\n */\nexport async function syncContactsWithUserStorage(\n config: SyncContactsWithUserStorageConfig,\n options: ContactSyncingOptions,\n): Promise<void> {\n const { getMessenger, getUserStorageControllerInstance, trace } = options;\n const {\n onContactSyncErroneousSituation,\n onContactUpdated,\n onContactDeleted,\n } = config;\n\n // Cannot perform sync, conditions not met\n if (!canPerformContactSyncing(options)) {\n return;\n }\n\n // NOTE: Pre-sync operations (canPerformContactSyncing, AddressBookController:list, getRemoteContacts)\n // are intentionally outside try-catch to let errors bubble up to Sentry for better debugging.\n // Only \"erroneous situation\" errors during sync logic itself should be caught.\n\n // Get all local contacts from AddressBookController (exclude chain \"*\" contacts)\n const localVisibleContacts =\n getMessenger()\n .call('AddressBookController:list')\n .filter((contact) => !isContactBridgedFromAccounts(contact))\n .filter(\n (contact) => contact.address && contact.chainId && contact.name?.trim(),\n ) || [];\n\n // Get remote contacts from user storage API\n const remoteContacts = await getRemoteContacts(options);\n\n // Filter remote contacts to exclude invalid ones (or empty array if no remote contacts)\n const validRemoteContacts =\n remoteContacts?.filter(\n (contact) => contact.address && contact.chainId && contact.name?.trim(),\n ) || [];\n\n const performSync = async () => {\n try {\n // Activate sync semaphore to prevent event loops\n await getUserStorageControllerInstance().setIsContactSyncingInProgress(\n true,\n );\n\n // Prepare maps for efficient lookup\n const localContactsMap = new Map<string, AddressBookEntry>();\n const remoteContactsMap = new Map<string, SyncAddressBookEntry>();\n\n localVisibleContacts.forEach((contact) => {\n const key = createContactKey(contact);\n localContactsMap.set(key, contact);\n });\n\n validRemoteContacts.forEach((contact) => {\n const key = createContactKey(contact);\n remoteContactsMap.set(key, contact);\n });\n\n // Lists to track contacts that need to be synced\n const contactsToAddOrUpdateLocally: SyncAddressBookEntry[] = [];\n const contactsToDeleteLocally: SyncAddressBookEntry[] = [];\n const contactsToUpdateRemotely: AddressBookEntry[] = [];\n\n // SCENARIO 2 & 6: Process remote contacts - handle new device sync and remote updates\n for (const remoteContact of validRemoteContacts) {\n const key = createContactKey(remoteContact);\n const localContact = localContactsMap.get(key);\n\n // Handle remote contact based on its status and local existence\n if (remoteContact.deletedAt) {\n // SCENARIO 8: Remote deletion - should be applied locally if contact exists locally\n if (localContact) {\n contactsToDeleteLocally.push(remoteContact);\n }\n } else if (!localContact) {\n // SCENARIO 2: New contact from remote - import to local\n contactsToAddOrUpdateLocally.push(remoteContact);\n } else {\n // SCENARIO 4 & 6: Contact exists on both sides - check for conflicts\n const hasContentDifference =\n localContact.name !== remoteContact.name ||\n localContact.memo !== remoteContact.memo;\n\n if (hasContentDifference) {\n // Check timestamps to determine which version to keep\n const localTimestamp = localContact.lastUpdatedAt || 0;\n const remoteTimestamp = remoteContact.lastUpdatedAt || 0;\n\n if (localTimestamp >= remoteTimestamp) {\n // Local is newer (or same age) - use local version\n contactsToUpdateRemotely.push(localContact);\n } else {\n // Remote is newer - use remote version\n contactsToAddOrUpdateLocally.push(remoteContact);\n }\n }\n\n // Else: content is identical, no action needed\n }\n }\n\n // SCENARIO 1, 3 & 5: Process local contacts not in remote - handles first sync and new local contacts\n for (const localContact of localVisibleContacts) {\n const key = createContactKey(localContact);\n const remoteContact = remoteContactsMap.get(key);\n\n if (!remoteContact) {\n // New local contact or first sync - add to remote\n contactsToUpdateRemotely.push(localContact);\n }\n }\n\n // Apply local deletions\n // Note: Individual errors are intentionally NOT caught here to ensure they reach Sentry\n // for debugging. Previous versions silently suppressed these errors which made\n // troubleshooting contact sync issues difficult.\n for (const contact of contactsToDeleteLocally) {\n getMessenger().call(\n 'AddressBookController:delete',\n contact.chainId,\n contact.address,\n );\n\n if (onContactDeleted) {\n onContactDeleted();\n }\n }\n\n // Apply local additions/updates\n // Note: Individual errors are intentionally NOT caught here to ensure they reach Sentry\n // for debugging. Previous versions silently suppressed these errors which made\n // troubleshooting contact sync issues difficult.\n for (const contact of contactsToAddOrUpdateLocally) {\n if (!contact.deletedAt) {\n getMessenger().call(\n 'AddressBookController:set',\n contact.address,\n contact.name || '',\n contact.chainId,\n contact.memo || '',\n contact.addressType,\n );\n\n if (onContactUpdated) {\n onContactUpdated();\n }\n }\n }\n\n // Apply changes to remote storage\n if (contactsToUpdateRemotely.length > 0) {\n const updatedRemoteContacts: Record<string, SyncAddressBookEntry> = {};\n for (const localContact of contactsToUpdateRemotely) {\n const key = createContactKey(localContact);\n updatedRemoteContacts[key] = {\n ...remoteContactsMap.get(key), // Start with an existing remote contact if it exists\n ...localContact, // override with local changes\n lastUpdatedAt: Date.now(), // mark as updated\n };\n }\n // Save updated contacts to remote storage\n await saveContactsToUserStorage(\n Object.values(updatedRemoteContacts),\n options,\n );\n }\n } catch (error) {\n if (onContactSyncErroneousSituation) {\n onContactSyncErroneousSituation('Error synchronizing contacts', {\n error,\n });\n\n // Re-throw the error to be handled by the caller\n throw error;\n }\n } finally {\n await getUserStorageControllerInstance().setIsContactSyncingInProgress(\n false,\n );\n }\n };\n\n if (trace) {\n // Gather pre-sync metrics for performance analysis\n const initialLocalContacts = localVisibleContacts;\n const initialValidRemoteContacts = validRemoteContacts;\n\n await trace(\n {\n name: TraceName.ContactSyncFull,\n data: {\n localContactCount: initialLocalContacts.length,\n remoteContactCount: initialValidRemoteContacts.length,\n isFirstSync:\n initialValidRemoteContacts.length === 0 &&\n initialLocalContacts.length > 0,\n isNewDeviceSync:\n initialLocalContacts.length === 0 &&\n initialValidRemoteContacts.length > 0,\n isRegularSync:\n initialLocalContacts.length > 0 &&\n initialValidRemoteContacts.length > 0,\n hasDataToSync:\n initialLocalContacts.length > 0 ||\n initialValidRemoteContacts.length > 0,\n expectedWorkload:\n initialLocalContacts.length + initialValidRemoteContacts.length,\n },\n },\n performSync,\n );\n\n return;\n }\n\n await performSync();\n}\n\n/**\n * Retrieves remote contacts from user storage API\n *\n * @param options - Parameters used for retrieving remote contacts\n * @returns Array of contacts from remote storage, or null if none found\n */\nasync function getRemoteContacts(\n options: ContactSyncingOptions,\n): Promise<SyncAddressBookEntry[] | null> {\n const { getUserStorageControllerInstance } = options;\n\n try {\n const remoteContactsJsonArray =\n await getUserStorageControllerInstance().performGetStorageAllFeatureEntries(\n USER_STORAGE_FEATURE_NAMES.addressBook,\n );\n\n if (!remoteContactsJsonArray || remoteContactsJsonArray.length === 0) {\n return null;\n }\n\n // Parse each JSON entry and convert from UserStorageContactEntry to AddressBookEntry\n const remoteStorageEntries = remoteContactsJsonArray.map((contactJson) => {\n const entry = JSON.parse(contactJson) as UserStorageContactEntry;\n return mapUserStorageEntryToAddressBookEntry(entry);\n });\n\n return remoteStorageEntries;\n } catch {\n return null;\n }\n}\n\n/**\n * Saves local contacts to user storage\n *\n * @param contacts - The contacts to save to user storage\n * @param options - Parameters used for saving contacts\n * @returns Promise that resolves when contacts are saved\n */\nasync function saveContactsToUserStorage(\n contacts: AddressBookEntry[],\n options: ContactSyncingOptions,\n): Promise<void> {\n const { getUserStorageControllerInstance, trace } = options;\n\n const saveContacts = async () => {\n if (!contacts || contacts.length === 0) {\n return;\n }\n\n // Convert each AddressBookEntry to UserStorageContactEntry format and create key-value pairs\n const storageEntries: [string, string][] = contacts.map((contact) => {\n const key = createContactKey(contact);\n const storageEntry = mapAddressBookEntryToUserStorageEntry(contact);\n return [key, JSON.stringify(storageEntry)];\n });\n\n await getUserStorageControllerInstance().performBatchSetStorage(\n USER_STORAGE_FEATURE_NAMES.addressBook,\n storageEntries,\n );\n };\n\n return trace\n ? await trace(\n {\n name: TraceName.ContactSyncSaveBatch,\n data: {\n contactCount: contacts.length,\n // Performance scaling indicators\n hasBatchOperations: contacts.length > 1,\n chainCount: new Set(contacts.map((c) => c.chainId)).size,\n hasMemosCount: contacts.filter((c) => c.memo?.length).length,\n },\n },\n saveContacts,\n )\n : await saveContacts();\n}\n\n/**\n * Updates a single contact in remote storage without performing a full sync\n * This is used when a contact is updated locally to efficiently push changes to remote\n *\n * @param contact - The contact that was updated locally\n * @param options - Parameters used for syncing operations\n * @returns Promise that resolves when the contact is updated\n */\nexport async function updateContactInRemoteStorage(\n contact: AddressBookEntry,\n options: ContactSyncingOptions,\n): Promise<void> {\n const { trace } = options;\n\n const updateContact = async () => {\n if (\n !canPerformContactSyncing(options) ||\n !contact.address ||\n !contact.chainId ||\n !contact.name?.trim()\n ) {\n return;\n }\n\n const { getUserStorageControllerInstance } = options;\n\n // Create an updated entry with timestamp\n const updatedEntry = {\n ...contact,\n lastUpdatedAt: contact.lastUpdatedAt || Date.now(),\n } as SyncAddressBookEntry;\n\n const key = createContactKey(contact);\n const storageEntry = mapAddressBookEntryToUserStorageEntry(updatedEntry);\n\n // Save individual contact to remote storage\n await getUserStorageControllerInstance().performSetStorage(\n `${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`,\n JSON.stringify(storageEntry),\n );\n };\n\n if (trace) {\n return await trace(\n {\n name: TraceName.ContactSyncUpdateRemote,\n data: {\n chainId: contact.chainId,\n // Performance indicators\n hasTimestamp: Boolean(contact.lastUpdatedAt),\n hasMemo: Boolean(contact.memo?.length),\n isUpdate: Boolean(contact.lastUpdatedAt), // vs new contact\n },\n },\n updateContact,\n );\n }\n\n return await updateContact();\n}\n\n/**\n * Marks a single contact as deleted in remote storage without performing a full sync\n * This is used when a contact is deleted locally to efficiently push the deletion to remote\n *\n * @param contact - The contact that was deleted locally (contains at least address and chainId)\n * @param options - Parameters used for syncing operations\n * @returns Promise that resolves when the contact is marked as deleted\n */\nexport async function deleteContactInRemoteStorage(\n contact: AddressBookEntry,\n options: ContactSyncingOptions,\n): Promise<void> {\n const { trace } = options;\n const deleteContact = async () => {\n if (\n !canPerformContactSyncing(options) ||\n !contact.address ||\n !contact.chainId ||\n !contact.name?.trim()\n ) {\n return;\n }\n\n const { getUserStorageControllerInstance } = options;\n const key = createContactKey(contact);\n\n try {\n // Try to get the existing contact first\n const existingContactJson =\n await getUserStorageControllerInstance().performGetStorage(\n `${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`,\n );\n\n if (existingContactJson) {\n // Mark the existing contact as deleted\n const existingStorageEntry = JSON.parse(\n existingContactJson,\n ) as UserStorageContactEntry;\n const existingContact =\n mapUserStorageEntryToAddressBookEntry(existingStorageEntry);\n\n const now = Date.now();\n const deletedContact = {\n ...existingContact,\n deletedAt: now,\n lastUpdatedAt: now,\n } as SyncAddressBookEntry;\n\n const deletedStorageEntry =\n mapAddressBookEntryToUserStorageEntry(deletedContact);\n\n // Save the deleted contact back to storage\n await getUserStorageControllerInstance().performSetStorage(\n `${USER_STORAGE_FEATURE_NAMES.addressBook}.${key}`,\n JSON.stringify(deletedStorageEntry),\n );\n }\n } catch {\n // If contact doesn't exist in remote storage, no need to mark as deleted\n console.warn('Contact not found in remote storage for deletion:', key);\n }\n };\n\n return trace\n ? await trace({ name: TraceName.ContactSyncDeleteRemote }, deleteContact)\n : await deleteContact();\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"types.cjs","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { Hex } from '@metamask/utils';\n\nimport type {\n USER_STORAGE_VERSION_KEY,\n USER_STORAGE_VERSION,\n} from './constants';\nimport type { UserStorageControllerMessenger } from '../UserStorageController';\nimport type UserStorageController from '../UserStorageController';\n\nexport type UserStorageContactEntry = {\n /**\n * The Version 'v' of the User Storage.\n * NOTE - will allow us to support upgrade/downgrades in the future\n */\n [USER_STORAGE_VERSION_KEY]: typeof USER_STORAGE_VERSION;\n /** the address 'a' of the contact */\n a: string;\n /** the name 'n' of the contact */\n n: string;\n /** the chainId 'c' of the contact */\n c: Hex;\n /** the memo 'm' of the contact (optional) */\n m?: string;\n /** the addressType 't' of the contact (optional) */\n t?: string;\n /** the isEns flag 'e' of the contact (optional) */\n e?: boolean;\n /** the lastUpdatedAt timestamp 'lu' of the contact */\n lu?: number;\n /** the deletedAt timestamp 'dt' of the contact (optional) */\n dt?: number;\n};\n\n/**\n * Options for contact syncing operations\n */\nexport type ContactSyncingOptions = {\n getUserStorageControllerInstance: () => UserStorageController;\n getMessenger: () => UserStorageControllerMessenger;\n};\n"]}
1
+ {"version":3,"file":"types.cjs","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { TraceCallback } from '@metamask/controller-utils';\nimport type { Hex } from '@metamask/utils';\n\nimport type {\n USER_STORAGE_VERSION_KEY,\n USER_STORAGE_VERSION,\n} from './constants';\nimport type { UserStorageControllerMessenger } from '../UserStorageController';\nimport type UserStorageController from '../UserStorageController';\n\nexport type UserStorageContactEntry = {\n /**\n * The Version 'v' of the User Storage.\n * NOTE - will allow us to support upgrade/downgrades in the future\n */\n [USER_STORAGE_VERSION_KEY]: typeof USER_STORAGE_VERSION;\n /** the address 'a' of the contact */\n a: string;\n /** the name 'n' of the contact */\n n: string;\n /** the chainId 'c' of the contact */\n c: Hex;\n /** the memo 'm' of the contact (optional) */\n m?: string;\n /** the addressType 't' of the contact (optional) */\n t?: string;\n /** the isEns flag 'e' of the contact (optional) */\n e?: boolean;\n /** the lastUpdatedAt timestamp 'lu' of the contact */\n lu?: number;\n /** the deletedAt timestamp 'dt' of the contact (optional) */\n dt?: number;\n};\n\n/**\n * Options for contact syncing operations\n */\nexport type ContactSyncingOptions = {\n getUserStorageControllerInstance: () => UserStorageController;\n getMessenger: () => UserStorageControllerMessenger;\n trace?: TraceCallback;\n};\n"]}
@@ -1,3 +1,4 @@
1
+ import type { TraceCallback } from "@metamask/controller-utils";
1
2
  import type { Hex } from "@metamask/utils";
2
3
  import type { USER_STORAGE_VERSION_KEY, USER_STORAGE_VERSION } from "./constants.cjs";
3
4
  import type { UserStorageControllerMessenger } from "../UserStorageController.cjs";
@@ -31,5 +32,6 @@ export type UserStorageContactEntry = {
31
32
  export type ContactSyncingOptions = {
32
33
  getUserStorageControllerInstance: () => UserStorageController;
33
34
  getMessenger: () => UserStorageControllerMessenger;
35
+ trace?: TraceCallback;
34
36
  };
35
37
  //# sourceMappingURL=types.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.cts","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,wBAAwB;AAE3C,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACrB,wBAAoB;AACrB,OAAO,KAAK,EAAE,8BAA8B,EAAE,qCAAiC;AAC/E,OAAO,KAAK,qBAAqB,qCAAiC;AAElE,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,CAAC,wBAAwB,CAAC,EAAE,OAAO,oBAAoB,CAAC;IACxD,qCAAqC;IACrC,CAAC,EAAE,MAAM,CAAC;IACV,kCAAkC;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,qCAAqC;IACrC,CAAC,EAAE,GAAG,CAAC;IACP,6CAA6C;IAC7C,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,CAAC,CAAC,EAAE,OAAO,CAAC;IACZ,sDAAsD;IACtD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;IAC9D,YAAY,EAAE,MAAM,8BAA8B,CAAC;CACpD,CAAC"}
1
+ {"version":3,"file":"types.d.cts","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,mCAAmC;AAChE,OAAO,KAAK,EAAE,GAAG,EAAE,wBAAwB;AAE3C,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACrB,wBAAoB;AACrB,OAAO,KAAK,EAAE,8BAA8B,EAAE,qCAAiC;AAC/E,OAAO,KAAK,qBAAqB,qCAAiC;AAElE,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,CAAC,wBAAwB,CAAC,EAAE,OAAO,oBAAoB,CAAC;IACxD,qCAAqC;IACrC,CAAC,EAAE,MAAM,CAAC;IACV,kCAAkC;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,qCAAqC;IACrC,CAAC,EAAE,GAAG,CAAC;IACP,6CAA6C;IAC7C,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,CAAC,CAAC,EAAE,OAAO,CAAC;IACZ,sDAAsD;IACtD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;IAC9D,YAAY,EAAE,MAAM,8BAA8B,CAAC;IACnD,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,CAAC"}
@@ -1,3 +1,4 @@
1
+ import type { TraceCallback } from "@metamask/controller-utils";
1
2
  import type { Hex } from "@metamask/utils";
2
3
  import type { USER_STORAGE_VERSION_KEY, USER_STORAGE_VERSION } from "./constants.mjs";
3
4
  import type { UserStorageControllerMessenger } from "../UserStorageController.mjs";
@@ -31,5 +32,6 @@ export type UserStorageContactEntry = {
31
32
  export type ContactSyncingOptions = {
32
33
  getUserStorageControllerInstance: () => UserStorageController;
33
34
  getMessenger: () => UserStorageControllerMessenger;
35
+ trace?: TraceCallback;
34
36
  };
35
37
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,wBAAwB;AAE3C,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACrB,wBAAoB;AACrB,OAAO,KAAK,EAAE,8BAA8B,EAAE,qCAAiC;AAC/E,OAAO,KAAK,qBAAqB,qCAAiC;AAElE,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,CAAC,wBAAwB,CAAC,EAAE,OAAO,oBAAoB,CAAC;IACxD,qCAAqC;IACrC,CAAC,EAAE,MAAM,CAAC;IACV,kCAAkC;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,qCAAqC;IACrC,CAAC,EAAE,GAAG,CAAC;IACP,6CAA6C;IAC7C,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,CAAC,CAAC,EAAE,OAAO,CAAC;IACZ,sDAAsD;IACtD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;IAC9D,YAAY,EAAE,MAAM,8BAA8B,CAAC;CACpD,CAAC"}
1
+ {"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,mCAAmC;AAChE,OAAO,KAAK,EAAE,GAAG,EAAE,wBAAwB;AAE3C,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACrB,wBAAoB;AACrB,OAAO,KAAK,EAAE,8BAA8B,EAAE,qCAAiC;AAC/E,OAAO,KAAK,qBAAqB,qCAAiC;AAElE,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;OAGG;IACH,CAAC,wBAAwB,CAAC,EAAE,OAAO,oBAAoB,CAAC;IACxD,qCAAqC;IACrC,CAAC,EAAE,MAAM,CAAC;IACV,kCAAkC;IAClC,CAAC,EAAE,MAAM,CAAC;IACV,qCAAqC;IACrC,CAAC,EAAE,GAAG,CAAC;IACP,6CAA6C;IAC7C,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,oDAAoD;IACpD,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,mDAAmD;IACnD,CAAC,CAAC,EAAE,OAAO,CAAC;IACZ,sDAAsD;IACtD,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;IAC9D,YAAY,EAAE,MAAM,8BAA8B,CAAC;IACnD,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,CAAC"}