@metamask-previews/profile-sync-controller 15.0.0-preview-ef2405f → 15.0.0-preview-e7141654

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 (74) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/controllers/user-storage/UserStorageController.cjs +68 -3
  3. package/dist/controllers/user-storage/UserStorageController.cjs.map +1 -1
  4. package/dist/controllers/user-storage/UserStorageController.d.cts +40 -2
  5. package/dist/controllers/user-storage/UserStorageController.d.cts.map +1 -1
  6. package/dist/controllers/user-storage/UserStorageController.d.mts +40 -2
  7. package/dist/controllers/user-storage/UserStorageController.d.mts.map +1 -1
  8. package/dist/controllers/user-storage/UserStorageController.mjs +65 -0
  9. package/dist/controllers/user-storage/UserStorageController.mjs.map +1 -1
  10. package/dist/controllers/user-storage/constants.cjs +1 -0
  11. package/dist/controllers/user-storage/constants.cjs.map +1 -1
  12. package/dist/controllers/user-storage/constants.d.cts +1 -0
  13. package/dist/controllers/user-storage/constants.d.cts.map +1 -1
  14. package/dist/controllers/user-storage/constants.d.mts +1 -0
  15. package/dist/controllers/user-storage/constants.d.mts.map +1 -1
  16. package/dist/controllers/user-storage/constants.mjs +1 -0
  17. package/dist/controllers/user-storage/constants.mjs.map +1 -1
  18. package/dist/controllers/user-storage/contact-syncing/constants.cjs +12 -0
  19. package/dist/controllers/user-storage/contact-syncing/constants.cjs.map +1 -0
  20. package/dist/controllers/user-storage/contact-syncing/constants.d.cts +9 -0
  21. package/dist/controllers/user-storage/contact-syncing/constants.d.cts.map +1 -0
  22. package/dist/controllers/user-storage/contact-syncing/constants.d.mts +9 -0
  23. package/dist/controllers/user-storage/contact-syncing/constants.d.mts.map +1 -0
  24. package/dist/controllers/user-storage/contact-syncing/constants.mjs +9 -0
  25. package/dist/controllers/user-storage/contact-syncing/constants.mjs.map +1 -0
  26. package/dist/controllers/user-storage/contact-syncing/controller-integration.cjs +376 -0
  27. package/dist/controllers/user-storage/contact-syncing/controller-integration.cjs.map +1 -0
  28. package/dist/controllers/user-storage/contact-syncing/controller-integration.d.cts +45 -0
  29. package/dist/controllers/user-storage/contact-syncing/controller-integration.d.cts.map +1 -0
  30. package/dist/controllers/user-storage/contact-syncing/controller-integration.d.mts +45 -0
  31. package/dist/controllers/user-storage/contact-syncing/controller-integration.d.mts.map +1 -0
  32. package/dist/controllers/user-storage/contact-syncing/controller-integration.mjs +370 -0
  33. package/dist/controllers/user-storage/contact-syncing/controller-integration.mjs.map +1 -0
  34. package/dist/controllers/user-storage/contact-syncing/setup-subscriptions.cjs +57 -0
  35. package/dist/controllers/user-storage/contact-syncing/setup-subscriptions.cjs.map +1 -0
  36. package/dist/controllers/user-storage/contact-syncing/setup-subscriptions.d.cts +8 -0
  37. package/dist/controllers/user-storage/contact-syncing/setup-subscriptions.d.cts.map +1 -0
  38. package/dist/controllers/user-storage/contact-syncing/setup-subscriptions.d.mts +8 -0
  39. package/dist/controllers/user-storage/contact-syncing/setup-subscriptions.d.mts.map +1 -0
  40. package/dist/controllers/user-storage/contact-syncing/setup-subscriptions.mjs +53 -0
  41. package/dist/controllers/user-storage/contact-syncing/setup-subscriptions.mjs.map +1 -0
  42. package/dist/controllers/user-storage/contact-syncing/sync-utils.cjs +23 -0
  43. package/dist/controllers/user-storage/contact-syncing/sync-utils.cjs.map +1 -0
  44. package/dist/controllers/user-storage/contact-syncing/sync-utils.d.cts +9 -0
  45. package/dist/controllers/user-storage/contact-syncing/sync-utils.d.cts.map +1 -0
  46. package/dist/controllers/user-storage/contact-syncing/sync-utils.d.mts +9 -0
  47. package/dist/controllers/user-storage/contact-syncing/sync-utils.d.mts.map +1 -0
  48. package/dist/controllers/user-storage/contact-syncing/sync-utils.mjs +19 -0
  49. package/dist/controllers/user-storage/contact-syncing/sync-utils.mjs.map +1 -0
  50. package/dist/controllers/user-storage/contact-syncing/types.cjs +3 -0
  51. package/dist/controllers/user-storage/contact-syncing/types.cjs.map +1 -0
  52. package/dist/controllers/user-storage/contact-syncing/types.d.cts +35 -0
  53. package/dist/controllers/user-storage/contact-syncing/types.d.cts.map +1 -0
  54. package/dist/controllers/user-storage/contact-syncing/types.d.mts +35 -0
  55. package/dist/controllers/user-storage/contact-syncing/types.d.mts.map +1 -0
  56. package/dist/controllers/user-storage/contact-syncing/types.mjs +2 -0
  57. package/dist/controllers/user-storage/contact-syncing/types.mjs.map +1 -0
  58. package/dist/controllers/user-storage/contact-syncing/utils.cjs +68 -0
  59. package/dist/controllers/user-storage/contact-syncing/utils.cjs.map +1 -0
  60. package/dist/controllers/user-storage/contact-syncing/utils.d.cts +29 -0
  61. package/dist/controllers/user-storage/contact-syncing/utils.d.cts.map +1 -0
  62. package/dist/controllers/user-storage/contact-syncing/utils.d.mts +29 -0
  63. package/dist/controllers/user-storage/contact-syncing/utils.d.mts.map +1 -0
  64. package/dist/controllers/user-storage/contact-syncing/utils.mjs +63 -0
  65. package/dist/controllers/user-storage/contact-syncing/utils.mjs.map +1 -0
  66. package/dist/shared/storage-schema.cjs +3 -1
  67. package/dist/shared/storage-schema.cjs.map +1 -1
  68. package/dist/shared/storage-schema.d.cts +2 -0
  69. package/dist/shared/storage-schema.d.cts.map +1 -1
  70. package/dist/shared/storage-schema.d.mts +2 -0
  71. package/dist/shared/storage-schema.d.mts.map +1 -1
  72. package/dist/shared/storage-schema.mjs +3 -1
  73. package/dist/shared/storage-schema.mjs.map +1 -1
  74. package/package.json +1 -1
@@ -0,0 +1,376 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deleteContactInRemoteStorage = exports.updateContactInRemoteStorage = exports.syncContactsWithUserStorage = exports.ADDRESS_BOOK_CONTACTS_KEY = void 0;
4
+ const sync_utils_1 = require("./sync-utils.cjs");
5
+ const utils_1 = require("./utils.cjs");
6
+ const storage_schema_1 = require("../../../shared/storage-schema.cjs");
7
+ // Define a constant to use as the key for storing all contacts
8
+ exports.ADDRESS_BOOK_CONTACTS_KEY = 'contacts';
9
+ /**
10
+ * Creates a unique key for a contact based on chainId and address
11
+ *
12
+ * @param contact - The contact to create a key for
13
+ * @returns A unique string key
14
+ */
15
+ function createContactKey(contact) {
16
+ return `${contact.chainId}:${contact.address.toLowerCase()}`;
17
+ }
18
+ /**
19
+ * Syncs contacts between local storage and user storage (remote).
20
+ *
21
+ * Handles the following syncing scenarios:
22
+ * 1. First Sync: When local contacts exist but there are no remote contacts, uploads all local contacts.
23
+ * 2. New Device Sync: Downloads remote contacts that don't exist locally (empty local address book).
24
+ * 3. Simple Merge: Ensures both sides (local & remote) have all contacts.
25
+ * 4. Contact Naming Conflicts: When same contact has different names, uses most recent by timestamp.
26
+ * 5. Local Updates: When a contact was updated locally, syncs changes to remote if local is newer.
27
+ * 6. Remote Updates: When a contact was updated remotely, applies changes locally if remote is newer.
28
+ * 7. Local Deletions: When a contact was deleted locally, marks it as deleted in remote storage.
29
+ * 8. Remote Deletions: When a contact was deleted remotely, applies deletion locally.
30
+ * 9. Concurrent Updates: Resolves conflicts using timestamps to determine the winner.
31
+ * 10. Restore After Delete: If a contact is modified after being deleted, restores it.
32
+ * 11. ChainId Differences: Treats same address on different chains as separate contacts.
33
+ *
34
+ * @param config - Parameters used for syncing callbacks
35
+ * @param options - Parameters used for syncing operations
36
+ */
37
+ async function syncContactsWithUserStorage(config, options) {
38
+ const { getMessenger, getUserStorageControllerInstance } = options;
39
+ const { onContactSyncErroneousSituation, onContactUpdated, onContactDeleted, } = config;
40
+ try {
41
+ if (!(0, sync_utils_1.canPerformContactSyncing)(options)) {
42
+ console.log('🔄 Contacts Sync: Cannot perform sync, conditions not met');
43
+ return;
44
+ }
45
+ console.log('🔄 Contacts Sync: Starting sync process');
46
+ // Activate sync semaphore to prevent event loops
47
+ await getUserStorageControllerInstance().setIsContactSyncingInProgress(true);
48
+ // Get all local contacts from AddressBookController (exclude chain "*" contacts)
49
+ const localVisibleContacts = getMessenger()
50
+ .call('AddressBookController:list')
51
+ .filter((contact) => String(contact.chainId) !== '*') || [];
52
+ console.log('🔄 Contacts Sync: Local contacts', localVisibleContacts);
53
+ // Get remote contacts from user storage API
54
+ const remoteContacts = await getRemoteContacts(options);
55
+ console.log('🔄 Contacts Sync: Remote contacts', remoteContacts);
56
+ // SCENARIO 1: First Sync - No remote contacts but local contacts exist
57
+ if (!remoteContacts || remoteContacts.length === 0) {
58
+ if (localVisibleContacts.length > 0) {
59
+ console.log('🔄 Contacts Sync: First sync - uploading local contacts to remote');
60
+ await saveContactsToUserStorage(localVisibleContacts, options);
61
+ }
62
+ return;
63
+ }
64
+ // Prepare maps for efficient lookup
65
+ const localContactsMap = new Map();
66
+ const remoteContactsMap = new Map();
67
+ localVisibleContacts.forEach((contact) => {
68
+ const key = createContactKey(contact);
69
+ localContactsMap.set(key, contact);
70
+ });
71
+ remoteContacts.forEach((contact) => {
72
+ const key = createContactKey(contact);
73
+ remoteContactsMap.set(key, contact);
74
+ });
75
+ // Lists to track contacts that need to be synced
76
+ const contactsToAddOrUpdateLocally = [];
77
+ const contactsToDeleteLocally = [];
78
+ const contactsToUpdateRemotely = [];
79
+ // SCENARIO 2 & 6: Process remote contacts - handle new device sync and remote updates
80
+ for (const remoteContact of remoteContacts) {
81
+ const key = createContactKey(remoteContact);
82
+ const localContact = localContactsMap.get(key);
83
+ // Handle remote contact based on its status and local existence
84
+ if (remoteContact.deleted) {
85
+ // SCENARIO 8: Remote deletion - should be applied locally if contact exists locally
86
+ if (localContact) {
87
+ console.log('🔄 Contacts Sync: Applying remote deletion locally', remoteContact);
88
+ contactsToDeleteLocally.push(remoteContact);
89
+ // Invoke deletion callback if needed
90
+ if (onContactDeleted) {
91
+ onContactDeleted();
92
+ }
93
+ }
94
+ }
95
+ else if (!localContact) {
96
+ // SCENARIO 2: New contact from remote - import to local
97
+ console.log('🔄 Contacts Sync: Importing new remote contact', remoteContact);
98
+ contactsToAddOrUpdateLocally.push(remoteContact);
99
+ // Invoke update callback if needed
100
+ if (onContactUpdated) {
101
+ onContactUpdated();
102
+ }
103
+ }
104
+ else {
105
+ // SCENARIO 4 & 6: Contact exists on both sides - check for conflicts
106
+ const hasContentDifference = localContact.name !== remoteContact.name ||
107
+ localContact.memo !== remoteContact.memo;
108
+ if (hasContentDifference) {
109
+ // Check timestamps to determine which version to keep
110
+ const localTimestamp = localContact.lastUpdatedAt || 0;
111
+ const remoteTimestamp = remoteContact.lastUpdatedAt || 0;
112
+ if (localTimestamp >= remoteTimestamp) {
113
+ // Local is newer (or same age) - use local version
114
+ console.log('🔄 Contacts Sync: Local wins by timestamp', {
115
+ local: localContact,
116
+ remote: remoteContact,
117
+ localTimestamp,
118
+ remoteTimestamp,
119
+ });
120
+ contactsToUpdateRemotely.push(localContact);
121
+ }
122
+ else {
123
+ // Remote is newer - use remote version
124
+ console.log('🔄 Contacts Sync: Remote wins by timestamp', {
125
+ local: localContact,
126
+ remote: remoteContact,
127
+ localTimestamp,
128
+ remoteTimestamp,
129
+ });
130
+ contactsToAddOrUpdateLocally.push(remoteContact);
131
+ // Invoke update callback if needed
132
+ if (onContactUpdated) {
133
+ onContactUpdated();
134
+ }
135
+ }
136
+ }
137
+ else {
138
+ // Content is identical, no action needed
139
+ console.log('🔄 Contacts Sync: Content identical, no action needed', {
140
+ local: localContact,
141
+ remote: remoteContact,
142
+ });
143
+ }
144
+ }
145
+ }
146
+ // SCENARIO 3 & 5: Process local contacts not in remote
147
+ for (const localContact of localVisibleContacts) {
148
+ const key = createContactKey(localContact);
149
+ const remoteContact = remoteContactsMap.get(key);
150
+ if (!remoteContact) {
151
+ // New local contact - add to remote
152
+ console.log('🔄 Contacts Sync: New local contact - adding to remote', localContact);
153
+ contactsToUpdateRemotely.push(localContact);
154
+ }
155
+ }
156
+ // SCENARIO 7: Detect local deletions
157
+ // Skip for new device scenario (no local contacts but remote has contacts)
158
+ const isNewDeviceScenario = localVisibleContacts.length === 0 && remoteContacts.length > 0;
159
+ if (!isNewDeviceScenario) {
160
+ // Find contacts that exist in remote but not locally (they were deleted locally)
161
+ const contactsToDeleteRemotely = [];
162
+ for (const remoteContact of remoteContacts) {
163
+ if (!remoteContact.deleted) {
164
+ const key = createContactKey(remoteContact);
165
+ if (!localContactsMap.has(key)) {
166
+ console.log('🔄 Contacts Sync: Contact deleted locally, marking as deleted in remote', remoteContact);
167
+ contactsToDeleteRemotely.push(remoteContact);
168
+ // Invoke deletion callback if needed
169
+ if (onContactDeleted) {
170
+ onContactDeleted();
171
+ }
172
+ }
173
+ }
174
+ }
175
+ // Mark contacts as deleted in remote
176
+ if (contactsToDeleteRemotely.length > 0) {
177
+ const remoteContactsWithDeletions = [...remoteContacts];
178
+ for (const contactToDelete of contactsToDeleteRemotely) {
179
+ const key = createContactKey(contactToDelete);
180
+ const indexToDelete = remoteContactsWithDeletions.findIndex((c) => createContactKey(c) === key);
181
+ if (indexToDelete !== -1) {
182
+ const now = Date.now();
183
+ remoteContactsWithDeletions[indexToDelete] = {
184
+ ...remoteContactsWithDeletions[indexToDelete],
185
+ deleted: true,
186
+ deletedAt: now,
187
+ lastUpdatedAt: now,
188
+ };
189
+ }
190
+ }
191
+ // Save updated remote contacts with deletions
192
+ await saveContactsToUserStorage(remoteContactsWithDeletions, options);
193
+ }
194
+ }
195
+ else {
196
+ console.log('🔄 Contacts Sync: Skipping local deletion detection - this is a new device');
197
+ }
198
+ // Apply local deletions
199
+ for (const contact of contactsToDeleteLocally) {
200
+ try {
201
+ console.log('🔄 Contacts Sync: Deleting contact locally', contact);
202
+ await getMessenger().call('AddressBookController:delete', contact.chainId, contact.address);
203
+ }
204
+ catch (error) {
205
+ console.error('Error deleting contact:', error);
206
+ }
207
+ }
208
+ // Apply local additions/updates
209
+ for (const contact of contactsToAddOrUpdateLocally) {
210
+ if (!contact.deleted) {
211
+ try {
212
+ console.log('🔄 Contacts Sync: Adding/updating contact locally', contact);
213
+ await getMessenger().call('AddressBookController:set', contact.address, contact.name || '', contact.chainId, contact.memo || '', contact.addressType);
214
+ }
215
+ catch (error) {
216
+ console.error('Error updating contact:', error);
217
+ }
218
+ }
219
+ }
220
+ // Apply changes to remote storage
221
+ if (contactsToUpdateRemotely.length > 0) {
222
+ console.log('🔄 Contacts Sync: Updating contacts in remote storage', contactsToUpdateRemotely);
223
+ // Update existing remote contacts with new contacts
224
+ const updatedRemoteContacts = [...remoteContacts];
225
+ for (const localContact of contactsToUpdateRemotely) {
226
+ const key = createContactKey(localContact);
227
+ const existingIndex = updatedRemoteContacts.findIndex((c) => createContactKey(c) === key);
228
+ const now = Date.now();
229
+ const updatedEntry = {
230
+ ...localContact,
231
+ lastUpdatedAt: now,
232
+ deleted: false, // Ensure it's not deleted
233
+ };
234
+ if (existingIndex !== -1) {
235
+ // Update existing contact
236
+ updatedRemoteContacts[existingIndex] = updatedEntry;
237
+ }
238
+ else {
239
+ // Add new contact
240
+ updatedRemoteContacts.push(updatedEntry);
241
+ }
242
+ }
243
+ // Save updated contacts to remote storage
244
+ await saveContactsToUserStorage(updatedRemoteContacts, options);
245
+ }
246
+ console.log('🔄 Contacts Sync: Sync completed successfully');
247
+ }
248
+ catch (error) {
249
+ console.error('🔄 Contacts Sync: Error during sync', error);
250
+ if (onContactSyncErroneousSituation) {
251
+ onContactSyncErroneousSituation('Error synchronizing contacts', {
252
+ error,
253
+ });
254
+ }
255
+ }
256
+ finally {
257
+ await getUserStorageControllerInstance().setIsContactSyncingInProgress(false);
258
+ }
259
+ }
260
+ exports.syncContactsWithUserStorage = syncContactsWithUserStorage;
261
+ /**
262
+ * Retrieves remote contacts from user storage API
263
+ *
264
+ * @param options - Parameters used for retrieving remote contacts
265
+ * @returns Array of contacts from remote storage, or null if none found
266
+ */
267
+ async function getRemoteContacts(options) {
268
+ const { getUserStorageControllerInstance } = options;
269
+ try {
270
+ const remoteContactsJson = await getUserStorageControllerInstance().performGetStorage(`${storage_schema_1.USER_STORAGE_FEATURE_NAMES.addressBook}.${exports.ADDRESS_BOOK_CONTACTS_KEY}`);
271
+ if (!remoteContactsJson) {
272
+ return null;
273
+ }
274
+ // Parse the JSON and convert each entry from UserStorageContactEntry to AddressBookEntry
275
+ const remoteStorageEntries = JSON.parse(remoteContactsJson);
276
+ return remoteStorageEntries.map((entry) => (0, utils_1.mapUserStorageEntryToAddressBookEntry)(entry));
277
+ }
278
+ catch {
279
+ return null;
280
+ }
281
+ }
282
+ /**
283
+ * Saves local contacts to user storage
284
+ *
285
+ * @param contacts - The contacts to save to user storage
286
+ * @param options - Parameters used for saving contacts
287
+ */
288
+ async function saveContactsToUserStorage(contacts, options) {
289
+ const { getUserStorageControllerInstance } = options;
290
+ if (!contacts || contacts.length === 0) {
291
+ return;
292
+ }
293
+ // Convert each AddressBookEntry to UserStorageContactEntry format before saving
294
+ const storageEntries = contacts.map((contact) => (0, utils_1.mapAddressBookEntryToUserStorageEntry)(contact));
295
+ await getUserStorageControllerInstance().performSetStorage(`${storage_schema_1.USER_STORAGE_FEATURE_NAMES.addressBook}.${exports.ADDRESS_BOOK_CONTACTS_KEY}`, JSON.stringify(storageEntries));
296
+ }
297
+ /**
298
+ * Updates a single contact in remote storage without performing a full sync
299
+ * This is used when a contact is updated locally to efficiently push changes to remote
300
+ *
301
+ * @param contact - The contact that was updated locally
302
+ * @param options - Parameters used for syncing operations
303
+ */
304
+ async function updateContactInRemoteStorage(contact, options) {
305
+ if (!(0, sync_utils_1.canPerformContactSyncing)(options)) {
306
+ return;
307
+ }
308
+ console.log('🔄 Contacts Sync: Updating single contact in remote storage', contact);
309
+ try {
310
+ // Get current remote contacts or initialize empty array
311
+ const remoteContacts = (await getRemoteContacts(options)) || [];
312
+ // Find if this contact already exists in remote
313
+ const key = createContactKey(contact);
314
+ const existingIndex = remoteContacts.findIndex((c) => createContactKey(c) === key);
315
+ const updatedRemoteContacts = [...remoteContacts];
316
+ // Create an updated entry with timestamp
317
+ const updatedEntry = {
318
+ ...contact,
319
+ lastUpdatedAt: contact.lastUpdatedAt || Date.now(),
320
+ deleted: false, // Explicitly set to false in case this was previously deleted
321
+ };
322
+ if (existingIndex !== -1) {
323
+ // Update existing contact
324
+ updatedRemoteContacts[existingIndex] = updatedEntry;
325
+ }
326
+ else {
327
+ // Add as new contact
328
+ updatedRemoteContacts.push(updatedEntry);
329
+ }
330
+ // Save to remote storage
331
+ await saveContactsToUserStorage(updatedRemoteContacts, options);
332
+ }
333
+ catch (error) {
334
+ console.error('🔄 Contacts Sync: Error updating contact in remote storage', error);
335
+ }
336
+ }
337
+ exports.updateContactInRemoteStorage = updateContactInRemoteStorage;
338
+ /**
339
+ * Marks a single contact as deleted in remote storage without performing a full sync
340
+ * This is used when a contact is deleted locally to efficiently push the deletion to remote
341
+ *
342
+ * @param contact - The contact that was deleted locally (contains at least address and chainId)
343
+ * @param options - Parameters used for syncing operations
344
+ */
345
+ async function deleteContactInRemoteStorage(contact, options) {
346
+ if (!(0, sync_utils_1.canPerformContactSyncing)(options)) {
347
+ return;
348
+ }
349
+ console.log('🔄 Contacts Sync: Marking contact as deleted in remote storage', contact);
350
+ try {
351
+ // Get current remote contacts
352
+ const remoteContacts = (await getRemoteContacts(options)) || [];
353
+ // Find the contact in remote storage
354
+ const key = createContactKey(contact);
355
+ const existingIndex = remoteContacts.findIndex((c) => createContactKey(c) === key);
356
+ // If the contact exists in remote storage
357
+ if (existingIndex !== -1) {
358
+ const updatedRemoteContacts = [...remoteContacts];
359
+ const now = Date.now();
360
+ // Mark the contact as deleted
361
+ updatedRemoteContacts[existingIndex] = {
362
+ ...updatedRemoteContacts[existingIndex],
363
+ deleted: true,
364
+ deletedAt: now,
365
+ lastUpdatedAt: now,
366
+ };
367
+ // Save to remote storage
368
+ await saveContactsToUserStorage(updatedRemoteContacts, options);
369
+ }
370
+ }
371
+ catch (error) {
372
+ console.error('🔄 Contacts Sync: Error marking contact as deleted in remote storage', error);
373
+ }
374
+ }
375
+ exports.deleteContactInRemoteStorage = deleteContactInRemoteStorage;
376
+ //# sourceMappingURL=controller-integration.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller-integration.cjs","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 { USER_STORAGE_FEATURE_NAMES } from '../../../shared/storage-schema';\n\n// Define a constant to use as the key for storing all contacts\nexport const ADDRESS_BOOK_CONTACTS_KEY = 'contacts';\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 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: When a contact was deleted locally, marks it as deleted in remote storage.\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 if (!canPerformContactSyncing(options)) {\n console.log('🔄 Contacts Sync: Cannot perform sync, conditions not met');\n return;\n }\n\n console.log('🔄 Contacts Sync: Starting sync process');\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) => String(contact.chainId) !== '*') || [];\n\n console.log('🔄 Contacts Sync: Local contacts', localVisibleContacts);\n\n // Get remote contacts from user storage API\n const remoteContacts = await getRemoteContacts(options);\n console.log('🔄 Contacts Sync: Remote contacts', remoteContacts);\n\n // SCENARIO 1: First Sync - No remote contacts but local contacts exist\n if (!remoteContacts || remoteContacts.length === 0) {\n if (localVisibleContacts.length > 0) {\n console.log(\n '🔄 Contacts Sync: First sync - uploading local contacts to remote',\n );\n await saveContactsToUserStorage(localVisibleContacts, options);\n }\n return;\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 remoteContacts.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 remoteContacts) {\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.deleted) {\n // SCENARIO 8: Remote deletion - should be applied locally if contact exists locally\n if (localContact) {\n console.log(\n '🔄 Contacts Sync: Applying remote deletion locally',\n remoteContact,\n );\n contactsToDeleteLocally.push(remoteContact);\n\n // Invoke deletion callback if needed\n if (onContactDeleted) {\n onContactDeleted();\n }\n }\n } else if (!localContact) {\n // SCENARIO 2: New contact from remote - import to local\n console.log(\n '🔄 Contacts Sync: Importing new remote contact',\n remoteContact,\n );\n contactsToAddOrUpdateLocally.push(remoteContact);\n\n // Invoke update callback if needed\n if (onContactUpdated) {\n onContactUpdated();\n }\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 console.log('🔄 Contacts Sync: Local wins by timestamp', {\n local: localContact,\n remote: remoteContact,\n localTimestamp,\n remoteTimestamp,\n });\n contactsToUpdateRemotely.push(localContact);\n } else {\n // Remote is newer - use remote version\n console.log('🔄 Contacts Sync: Remote wins by timestamp', {\n local: localContact,\n remote: remoteContact,\n localTimestamp,\n remoteTimestamp,\n });\n contactsToAddOrUpdateLocally.push(remoteContact);\n\n // Invoke update callback if needed\n if (onContactUpdated) {\n onContactUpdated();\n }\n }\n } else {\n // Content is identical, no action needed\n console.log('🔄 Contacts Sync: Content identical, no action needed', {\n local: localContact,\n remote: remoteContact,\n });\n }\n }\n }\n\n // SCENARIO 3 & 5: Process local contacts not in remote\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 - add to remote\n console.log(\n '🔄 Contacts Sync: New local contact - adding to remote',\n localContact,\n );\n contactsToUpdateRemotely.push(localContact);\n }\n }\n\n // SCENARIO 7: Detect local deletions\n // Skip for new device scenario (no local contacts but remote has contacts)\n const isNewDeviceScenario =\n localVisibleContacts.length === 0 && remoteContacts.length > 0;\n\n if (!isNewDeviceScenario) {\n // Find contacts that exist in remote but not locally (they were deleted locally)\n const contactsToDeleteRemotely: SyncAddressBookEntry[] = [];\n\n for (const remoteContact of remoteContacts) {\n if (!remoteContact.deleted) {\n const key = createContactKey(remoteContact);\n if (!localContactsMap.has(key)) {\n console.log(\n '🔄 Contacts Sync: Contact deleted locally, marking as deleted in remote',\n remoteContact,\n );\n contactsToDeleteRemotely.push(remoteContact);\n\n // Invoke deletion callback if needed\n if (onContactDeleted) {\n onContactDeleted();\n }\n }\n }\n }\n\n // Mark contacts as deleted in remote\n if (contactsToDeleteRemotely.length > 0) {\n const remoteContactsWithDeletions = [...remoteContacts];\n\n for (const contactToDelete of contactsToDeleteRemotely) {\n const key = createContactKey(contactToDelete);\n const indexToDelete = remoteContactsWithDeletions.findIndex(\n (c) => createContactKey(c) === key,\n );\n\n if (indexToDelete !== -1) {\n const now = Date.now();\n remoteContactsWithDeletions[indexToDelete] = {\n ...remoteContactsWithDeletions[indexToDelete],\n deleted: true,\n deletedAt: now,\n lastUpdatedAt: now,\n };\n }\n }\n\n // Save updated remote contacts with deletions\n await saveContactsToUserStorage(remoteContactsWithDeletions, options);\n }\n } else {\n console.log(\n '🔄 Contacts Sync: Skipping local deletion detection - this is a new device',\n );\n }\n\n // Apply local deletions\n for (const contact of contactsToDeleteLocally) {\n try {\n console.log('🔄 Contacts Sync: Deleting contact locally', contact);\n await getMessenger().call(\n 'AddressBookController:delete',\n contact.chainId,\n contact.address,\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.deleted) {\n try {\n console.log(\n '🔄 Contacts Sync: Adding/updating contact locally',\n contact,\n );\n await getMessenger().call(\n 'AddressBookController:set',\n contact.address,\n contact.name || '',\n contact.chainId,\n contact.memo || '',\n contact.addressType,\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 console.log(\n '🔄 Contacts Sync: Updating contacts in remote storage',\n contactsToUpdateRemotely,\n );\n\n // Update existing remote contacts with new contacts\n const updatedRemoteContacts = [...remoteContacts];\n\n for (const localContact of contactsToUpdateRemotely) {\n const key = createContactKey(localContact);\n const existingIndex = updatedRemoteContacts.findIndex(\n (c) => createContactKey(c) === key,\n );\n\n const now = Date.now();\n const updatedEntry = {\n ...localContact,\n lastUpdatedAt: now,\n deleted: false, // Ensure it's not deleted\n } as SyncAddressBookEntry;\n\n if (existingIndex !== -1) {\n // Update existing contact\n updatedRemoteContacts[existingIndex] = updatedEntry;\n } else {\n // Add new contact\n updatedRemoteContacts.push(updatedEntry);\n }\n }\n\n // Save updated contacts to remote storage\n await saveContactsToUserStorage(updatedRemoteContacts, options);\n }\n\n console.log('🔄 Contacts Sync: Sync completed successfully');\n } catch (error) {\n console.error('🔄 Contacts Sync: Error during sync', error);\n if (onContactSyncErroneousSituation) {\n onContactSyncErroneousSituation('Error synchronizing contacts', {\n error,\n });\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 remoteContactsJson =\n await getUserStorageControllerInstance().performGetStorage(\n `${USER_STORAGE_FEATURE_NAMES.addressBook}.${ADDRESS_BOOK_CONTACTS_KEY}`,\n );\n\n if (!remoteContactsJson) {\n return null;\n }\n\n // Parse the JSON and convert each entry from UserStorageContactEntry to AddressBookEntry\n const remoteStorageEntries = JSON.parse(\n remoteContactsJson,\n ) as UserStorageContactEntry[];\n return remoteStorageEntries.map((entry) =>\n mapUserStorageEntryToAddressBookEntry(entry),\n );\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 before saving\n const storageEntries = contacts.map((contact) =>\n mapAddressBookEntryToUserStorageEntry(contact),\n );\n\n await getUserStorageControllerInstance().performSetStorage(\n `${USER_STORAGE_FEATURE_NAMES.addressBook}.${ADDRESS_BOOK_CONTACTS_KEY}`,\n JSON.stringify(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 (!canPerformContactSyncing(options)) {\n return;\n }\n\n console.log(\n '🔄 Contacts Sync: Updating single contact in remote storage',\n contact,\n );\n\n try {\n // Get current remote contacts or initialize empty array\n const remoteContacts = (await getRemoteContacts(options)) || [];\n\n // Find if this contact already exists in remote\n const key = createContactKey(contact);\n const existingIndex = remoteContacts.findIndex(\n (c) => createContactKey(c) === key,\n );\n\n const updatedRemoteContacts = [...remoteContacts];\n\n // Create an updated entry with timestamp\n const updatedEntry = {\n ...contact,\n lastUpdatedAt: contact.lastUpdatedAt || Date.now(),\n deleted: false, // Explicitly set to false in case this was previously deleted\n } as SyncAddressBookEntry;\n\n if (existingIndex !== -1) {\n // Update existing contact\n updatedRemoteContacts[existingIndex] = updatedEntry;\n } else {\n // Add as new contact\n updatedRemoteContacts.push(updatedEntry);\n }\n\n // Save to remote storage\n await saveContactsToUserStorage(updatedRemoteContacts, options);\n } catch (error) {\n console.error(\n '🔄 Contacts Sync: Error updating contact in remote storage',\n error,\n );\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 (!canPerformContactSyncing(options)) {\n return;\n }\n\n console.log(\n '🔄 Contacts Sync: Marking contact as deleted in remote storage',\n contact,\n );\n\n try {\n // Get current remote contacts\n const remoteContacts = (await getRemoteContacts(options)) || [];\n\n // Find the contact in remote storage\n const key = createContactKey(contact);\n const existingIndex = remoteContacts.findIndex(\n (c) => createContactKey(c) === key,\n );\n\n // If the contact exists in remote storage\n if (existingIndex !== -1) {\n const updatedRemoteContacts = [...remoteContacts];\n const now = Date.now();\n\n // Mark the contact as deleted\n updatedRemoteContacts[existingIndex] = {\n ...updatedRemoteContacts[existingIndex],\n deleted: true,\n deletedAt: now,\n lastUpdatedAt: now,\n };\n\n // Save to remote storage\n await saveContactsToUserStorage(updatedRemoteContacts, options);\n }\n } catch (error) {\n console.error(\n '🔄 Contacts Sync: Error marking contact as deleted in remote storage',\n error,\n );\n }\n}\n"]}
@@ -0,0 +1,45 @@
1
+ import type { AddressBookEntry } from "@metamask/address-book-controller";
2
+ import type { ContactSyncingOptions } from "./types.cjs";
3
+ export declare const ADDRESS_BOOK_CONTACTS_KEY = "contacts";
4
+ export type SyncContactsWithUserStorageConfig = {
5
+ onContactSyncErroneousSituation?: (errorMessage: string, sentryContext?: Record<string, unknown>) => void;
6
+ onContactUpdated?: () => void;
7
+ onContactDeleted?: () => void;
8
+ };
9
+ /**
10
+ * Syncs contacts between local storage and user storage (remote).
11
+ *
12
+ * Handles the following syncing scenarios:
13
+ * 1. First Sync: When local contacts exist but there are no remote contacts, uploads all local contacts.
14
+ * 2. New Device Sync: Downloads remote contacts that don't exist locally (empty local address book).
15
+ * 3. Simple Merge: Ensures both sides (local & remote) have all contacts.
16
+ * 4. Contact Naming Conflicts: When same contact has different names, uses most recent by timestamp.
17
+ * 5. Local Updates: When a contact was updated locally, syncs changes to remote if local is newer.
18
+ * 6. Remote Updates: When a contact was updated remotely, applies changes locally if remote is newer.
19
+ * 7. Local Deletions: When a contact was deleted locally, marks it as deleted in remote storage.
20
+ * 8. Remote Deletions: When a contact was deleted remotely, applies deletion locally.
21
+ * 9. Concurrent Updates: Resolves conflicts using timestamps to determine the winner.
22
+ * 10. Restore After Delete: If a contact is modified after being deleted, restores it.
23
+ * 11. ChainId Differences: Treats same address on different chains as separate contacts.
24
+ *
25
+ * @param config - Parameters used for syncing callbacks
26
+ * @param options - Parameters used for syncing operations
27
+ */
28
+ export declare function syncContactsWithUserStorage(config: SyncContactsWithUserStorageConfig, options: ContactSyncingOptions): Promise<void>;
29
+ /**
30
+ * Updates a single contact in remote storage without performing a full sync
31
+ * This is used when a contact is updated locally to efficiently push changes to remote
32
+ *
33
+ * @param contact - The contact that was updated locally
34
+ * @param options - Parameters used for syncing operations
35
+ */
36
+ export declare function updateContactInRemoteStorage(contact: AddressBookEntry, options: ContactSyncingOptions): Promise<void>;
37
+ /**
38
+ * Marks a single contact as deleted in remote storage without performing a full sync
39
+ * This is used when a contact is deleted locally to efficiently push the deletion to remote
40
+ *
41
+ * @param contact - The contact that was deleted locally (contains at least address and chainId)
42
+ * @param options - Parameters used for syncing operations
43
+ */
44
+ export declare function deleteContactInRemoteStorage(contact: AddressBookEntry, options: ContactSyncingOptions): Promise<void>;
45
+ //# sourceMappingURL=controller-integration.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller-integration.d.cts","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/controller-integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,0CAA0C;AAG1E,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAgB;AAUrD,eAAO,MAAM,yBAAyB,aAAa,CAAC;AAEpD,MAAM,MAAM,iCAAiC,GAAG;IAC9C,+BAA+B,CAAC,EAAE,CAChC,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACpC,IAAI,CAAC;IACV,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B,CAAC;AAYF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,iCAAiC,EACzC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAySf;AA8DD;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CA6Cf;AAED;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CA0Cf"}
@@ -0,0 +1,45 @@
1
+ import type { AddressBookEntry } from "@metamask/address-book-controller";
2
+ import type { ContactSyncingOptions } from "./types.mjs";
3
+ export declare const ADDRESS_BOOK_CONTACTS_KEY = "contacts";
4
+ export type SyncContactsWithUserStorageConfig = {
5
+ onContactSyncErroneousSituation?: (errorMessage: string, sentryContext?: Record<string, unknown>) => void;
6
+ onContactUpdated?: () => void;
7
+ onContactDeleted?: () => void;
8
+ };
9
+ /**
10
+ * Syncs contacts between local storage and user storage (remote).
11
+ *
12
+ * Handles the following syncing scenarios:
13
+ * 1. First Sync: When local contacts exist but there are no remote contacts, uploads all local contacts.
14
+ * 2. New Device Sync: Downloads remote contacts that don't exist locally (empty local address book).
15
+ * 3. Simple Merge: Ensures both sides (local & remote) have all contacts.
16
+ * 4. Contact Naming Conflicts: When same contact has different names, uses most recent by timestamp.
17
+ * 5. Local Updates: When a contact was updated locally, syncs changes to remote if local is newer.
18
+ * 6. Remote Updates: When a contact was updated remotely, applies changes locally if remote is newer.
19
+ * 7. Local Deletions: When a contact was deleted locally, marks it as deleted in remote storage.
20
+ * 8. Remote Deletions: When a contact was deleted remotely, applies deletion locally.
21
+ * 9. Concurrent Updates: Resolves conflicts using timestamps to determine the winner.
22
+ * 10. Restore After Delete: If a contact is modified after being deleted, restores it.
23
+ * 11. ChainId Differences: Treats same address on different chains as separate contacts.
24
+ *
25
+ * @param config - Parameters used for syncing callbacks
26
+ * @param options - Parameters used for syncing operations
27
+ */
28
+ export declare function syncContactsWithUserStorage(config: SyncContactsWithUserStorageConfig, options: ContactSyncingOptions): Promise<void>;
29
+ /**
30
+ * Updates a single contact in remote storage without performing a full sync
31
+ * This is used when a contact is updated locally to efficiently push changes to remote
32
+ *
33
+ * @param contact - The contact that was updated locally
34
+ * @param options - Parameters used for syncing operations
35
+ */
36
+ export declare function updateContactInRemoteStorage(contact: AddressBookEntry, options: ContactSyncingOptions): Promise<void>;
37
+ /**
38
+ * Marks a single contact as deleted in remote storage without performing a full sync
39
+ * This is used when a contact is deleted locally to efficiently push the deletion to remote
40
+ *
41
+ * @param contact - The contact that was deleted locally (contains at least address and chainId)
42
+ * @param options - Parameters used for syncing operations
43
+ */
44
+ export declare function deleteContactInRemoteStorage(contact: AddressBookEntry, options: ContactSyncingOptions): Promise<void>;
45
+ //# sourceMappingURL=controller-integration.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller-integration.d.mts","sourceRoot":"","sources":["../../../../src/controllers/user-storage/contact-syncing/controller-integration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,0CAA0C;AAG1E,OAAO,KAAK,EAAE,qBAAqB,EAAE,oBAAgB;AAUrD,eAAO,MAAM,yBAAyB,aAAa,CAAC;AAEpD,MAAM,MAAM,iCAAiC,GAAG;IAC9C,+BAA+B,CAAC,EAAE,CAChC,YAAY,EAAE,MAAM,EACpB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACpC,IAAI,CAAC;IACV,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;CAC/B,CAAC;AAYF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,iCAAiC,EACzC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAySf;AA8DD;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CA6Cf;AAED;;;;;;GAMG;AACH,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CA0Cf"}