@metamask/assets-controller 9.1.0 → 10.0.1

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 (76) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/dist/AssetsController.cjs +351 -90
  3. package/dist/AssetsController.cjs.map +1 -1
  4. package/dist/AssetsController.d.cts +11 -8
  5. package/dist/AssetsController.d.cts.map +1 -1
  6. package/dist/AssetsController.d.mts +11 -8
  7. package/dist/AssetsController.d.mts.map +1 -1
  8. package/dist/AssetsController.mjs +351 -90
  9. package/dist/AssetsController.mjs.map +1 -1
  10. package/dist/data-sources/AccountsApiDataSource.cjs +14 -1
  11. package/dist/data-sources/AccountsApiDataSource.cjs.map +1 -1
  12. package/dist/data-sources/AccountsApiDataSource.d.cts +8 -0
  13. package/dist/data-sources/AccountsApiDataSource.d.cts.map +1 -1
  14. package/dist/data-sources/AccountsApiDataSource.d.mts +8 -0
  15. package/dist/data-sources/AccountsApiDataSource.d.mts.map +1 -1
  16. package/dist/data-sources/AccountsApiDataSource.mjs +14 -1
  17. package/dist/data-sources/AccountsApiDataSource.mjs.map +1 -1
  18. package/dist/data-sources/BackendWebsocketDataSource.cjs +236 -149
  19. package/dist/data-sources/BackendWebsocketDataSource.cjs.map +1 -1
  20. package/dist/data-sources/BackendWebsocketDataSource.d.cts +15 -0
  21. package/dist/data-sources/BackendWebsocketDataSource.d.cts.map +1 -1
  22. package/dist/data-sources/BackendWebsocketDataSource.d.mts +15 -0
  23. package/dist/data-sources/BackendWebsocketDataSource.d.mts.map +1 -1
  24. package/dist/data-sources/BackendWebsocketDataSource.mjs +236 -146
  25. package/dist/data-sources/BackendWebsocketDataSource.mjs.map +1 -1
  26. package/dist/data-sources/PriceDataSource.cjs +18 -9
  27. package/dist/data-sources/PriceDataSource.cjs.map +1 -1
  28. package/dist/data-sources/PriceDataSource.d.cts.map +1 -1
  29. package/dist/data-sources/PriceDataSource.d.mts.map +1 -1
  30. package/dist/data-sources/PriceDataSource.mjs +19 -10
  31. package/dist/data-sources/PriceDataSource.mjs.map +1 -1
  32. package/dist/data-sources/RpcDataSource.cjs +29 -9
  33. package/dist/data-sources/RpcDataSource.cjs.map +1 -1
  34. package/dist/data-sources/RpcDataSource.d.cts +5 -0
  35. package/dist/data-sources/RpcDataSource.d.cts.map +1 -1
  36. package/dist/data-sources/RpcDataSource.d.mts +5 -0
  37. package/dist/data-sources/RpcDataSource.d.mts.map +1 -1
  38. package/dist/data-sources/RpcDataSource.mjs +29 -9
  39. package/dist/data-sources/RpcDataSource.mjs.map +1 -1
  40. package/dist/middlewares/DetectionMiddleware.cjs +22 -1
  41. package/dist/middlewares/DetectionMiddleware.cjs.map +1 -1
  42. package/dist/middlewares/DetectionMiddleware.d.cts +3 -0
  43. package/dist/middlewares/DetectionMiddleware.d.cts.map +1 -1
  44. package/dist/middlewares/DetectionMiddleware.d.mts +3 -0
  45. package/dist/middlewares/DetectionMiddleware.d.mts.map +1 -1
  46. package/dist/middlewares/DetectionMiddleware.mjs +22 -1
  47. package/dist/middlewares/DetectionMiddleware.mjs.map +1 -1
  48. package/dist/middlewares/ParallelMiddleware.cjs +7 -0
  49. package/dist/middlewares/ParallelMiddleware.cjs.map +1 -1
  50. package/dist/middlewares/ParallelMiddleware.d.cts.map +1 -1
  51. package/dist/middlewares/ParallelMiddleware.d.mts.map +1 -1
  52. package/dist/middlewares/ParallelMiddleware.mjs +7 -0
  53. package/dist/middlewares/ParallelMiddleware.mjs.map +1 -1
  54. package/dist/types.cjs.map +1 -1
  55. package/dist/types.d.cts +20 -4
  56. package/dist/types.d.cts.map +1 -1
  57. package/dist/types.d.mts +20 -4
  58. package/dist/types.d.mts.map +1 -1
  59. package/dist/types.mjs.map +1 -1
  60. package/dist/utils/dedupingBatchFetcher.cjs +11 -0
  61. package/dist/utils/dedupingBatchFetcher.cjs.map +1 -1
  62. package/dist/utils/dedupingBatchFetcher.d.cts +7 -0
  63. package/dist/utils/dedupingBatchFetcher.d.cts.map +1 -1
  64. package/dist/utils/dedupingBatchFetcher.d.mts +7 -0
  65. package/dist/utils/dedupingBatchFetcher.d.mts.map +1 -1
  66. package/dist/utils/dedupingBatchFetcher.mjs +11 -0
  67. package/dist/utils/dedupingBatchFetcher.mjs.map +1 -1
  68. package/dist/utils/processAccountActivityBalanceUpdates.cjs +54 -0
  69. package/dist/utils/processAccountActivityBalanceUpdates.cjs.map +1 -0
  70. package/dist/utils/processAccountActivityBalanceUpdates.d.cts +13 -0
  71. package/dist/utils/processAccountActivityBalanceUpdates.d.cts.map +1 -0
  72. package/dist/utils/processAccountActivityBalanceUpdates.d.mts +13 -0
  73. package/dist/utils/processAccountActivityBalanceUpdates.d.mts.map +1 -0
  74. package/dist/utils/processAccountActivityBalanceUpdates.mjs +47 -0
  75. package/dist/utils/processAccountActivityBalanceUpdates.mjs.map +1 -0
  76. package/package.json +8 -8
@@ -10,15 +10,12 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var __importDefault = (this && this.__importDefault) || function (mod) {
14
- return (mod && mod.__esModule) ? mod : { "default": mod };
15
- };
16
- var _BackendWebsocketDataSource_instances, _BackendWebsocketDataSource_messenger, _BackendWebsocketDataSource_apiClient, _BackendWebsocketDataSource_onActiveChainsUpdated, _BackendWebsocketDataSource_getAssetType, _BackendWebsocketDataSource_chainsRefreshTimer, _BackendWebsocketDataSource_supportedChains, _BackendWebsocketDataSource_isConnected, _BackendWebsocketDataSource_wsSubscriptions, _BackendWebsocketDataSource_pendingSubscriptions, _BackendWebsocketDataSource_subscriptionRequests, _BackendWebsocketDataSource_initializeActiveChains, _BackendWebsocketDataSource_refreshActiveChains, _BackendWebsocketDataSource_fetchActiveChains, _BackendWebsocketDataSource_subscribeToEvents, _BackendWebsocketDataSource_handleDisconnect, _BackendWebsocketDataSource_handleReconnect, _BackendWebsocketDataSource_handleNotification, _BackendWebsocketDataSource_processBalanceUpdates;
13
+ var _BackendWebsocketDataSource_instances, _BackendWebsocketDataSource_messenger, _BackendWebsocketDataSource_apiClient, _BackendWebsocketDataSource_onActiveChainsUpdated, _BackendWebsocketDataSource_getAssetType, _BackendWebsocketDataSource_chainsRefreshTimer, _BackendWebsocketDataSource_supportedChains, _BackendWebsocketDataSource_isConnected, _BackendWebsocketDataSource_wsSubscriptions, _BackendWebsocketDataSource_pendingSubscriptions, _BackendWebsocketDataSource_subscriptionRequests, _BackendWebsocketDataSource_registeredChannelCallbacks, _BackendWebsocketDataSource_subscribeLock, _BackendWebsocketDataSource_initializeActiveChains, _BackendWebsocketDataSource_refreshActiveChains, _BackendWebsocketDataSource_fetchActiveChains, _BackendWebsocketDataSource_subscribeToEvents, _BackendWebsocketDataSource_handleDisconnect, _BackendWebsocketDataSource_handleReconnect, _BackendWebsocketDataSource_subscribeInternal, _BackendWebsocketDataSource_handleNotification, _BackendWebsocketDataSource_processBalanceUpdates, _BackendWebsocketDataSource_teardownSubscription, _BackendWebsocketDataSource_registerChannelCallbacks, _BackendWebsocketDataSource_unregisterChannelCallback, _BackendWebsocketDataSource_removeChannelCallbacks;
17
14
  Object.defineProperty(exports, "__esModule", { value: true });
18
15
  exports.createBackendWebsocketDataSource = exports.BackendWebsocketDataSource = void 0;
19
16
  const utils_1 = require("@metamask/utils");
20
- const bignumber_js_1 = __importDefault(require("bignumber.js"));
21
17
  const logger_1 = require("../logger.cjs");
18
+ const processAccountActivityBalanceUpdates_1 = require("../utils/processAccountActivityBalanceUpdates.cjs");
22
19
  const AbstractDataSource_1 = require("./AbstractDataSource.cjs");
23
20
  // ============================================================================
24
21
  // CONSTANTS
@@ -95,6 +92,34 @@ function buildAccountActivityChannel(namespace, address) {
95
92
  const formatted = namespace === 'eip155' ? address.toLowerCase() : address;
96
93
  return `${CHANNEL_TYPE}.${namespace}:0:${formatted}`;
97
94
  }
95
+ /**
96
+ * Normalize addresses for stable comparison when detecting account changes.
97
+ *
98
+ * @param address - Account address (hex or base58).
99
+ * @returns Normalized address for comparison.
100
+ */
101
+ function normalizeAddressForComparison(address) {
102
+ return address.startsWith('0x') ? address.toLowerCase() : address;
103
+ }
104
+ /**
105
+ * Check whether subscribed account addresses changed (case-insensitive for EVM).
106
+ *
107
+ * @param nextAddresses - Addresses from the incoming subscribe request.
108
+ * @param existingAddresses - Addresses from the active subscription.
109
+ * @returns True when the address sets differ.
110
+ */
111
+ function haveAddressesChanged(nextAddresses, existingAddresses) {
112
+ if (nextAddresses.length !== existingAddresses.length) {
113
+ return true;
114
+ }
115
+ const normalizedNext = nextAddresses
116
+ .map(normalizeAddressForComparison)
117
+ .sort();
118
+ const normalizedExisting = existingAddresses
119
+ .map(normalizeAddressForComparison)
120
+ .sort();
121
+ return normalizedNext.some((address, index) => address !== normalizedExisting[index]);
122
+ }
98
123
  /**
99
124
  * Normalize API chain identifier to CAIP-2 ChainId.
100
125
  * Passes through strings already in CAIP-2 form (e.g. eip155:1, solana:5eykt...).
@@ -142,6 +167,8 @@ function toChainId(chainIdOrDecimal) {
142
167
  * - BackendWebSocketService:subscribe
143
168
  * - BackendWebSocketService:getConnectionInfo
144
169
  * - BackendWebSocketService:findSubscriptionsByChannelPrefix
170
+ * - BackendWebSocketService:addChannelCallback
171
+ * - BackendWebSocketService:removeChannelCallback
145
172
  */
146
173
  const DEFAULT_CHAINS_REFRESH_INTERVAL_MS = 20 * 60 * 1000; // 20 minutes
147
174
  class BackendWebsocketDataSource extends AbstractDataSource_1.AbstractDataSource {
@@ -167,6 +194,10 @@ class BackendWebsocketDataSource extends AbstractDataSource_1.AbstractDataSource
167
194
  _BackendWebsocketDataSource_pendingSubscriptions.set(this, new Map());
168
195
  /** Store original subscription requests for reconnection */
169
196
  _BackendWebsocketDataSource_subscriptionRequests.set(this, new Map());
197
+ /** Channels with registered BackendWebSocketService channel callbacks */
198
+ _BackendWebsocketDataSource_registeredChannelCallbacks.set(this, new Set());
199
+ /** Serializes subscribe/unsubscribe so account switches cannot interleave. */
200
+ _BackendWebsocketDataSource_subscribeLock.set(this, Promise.resolve());
170
201
  __classPrivateFieldSet(this, _BackendWebsocketDataSource_messenger, options.messenger, "f");
171
202
  __classPrivateFieldSet(this, _BackendWebsocketDataSource_apiClient, options.queryApiClient, "f");
172
203
  __classPrivateFieldSet(this, _BackendWebsocketDataSource_onActiveChainsUpdated, options.onActiveChainsUpdated, "f");
@@ -174,6 +205,16 @@ class BackendWebsocketDataSource extends AbstractDataSource_1.AbstractDataSource
174
205
  __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_subscribeToEvents).call(this);
175
206
  __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_initializeActiveChains).call(this).catch(console.error);
176
207
  }
208
+ /**
209
+ * Re-fetch supported networks and refresh `activeChains` when connected.
210
+ * When disconnected, only `#supportedChains` is updated so reconnect can
211
+ * reclaim chains. Called on EVM network switch from AssetsController.
212
+ *
213
+ * @returns Resolves when supported networks have been re-fetched.
214
+ */
215
+ refreshActiveChains() {
216
+ return __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_refreshActiveChains).call(this);
217
+ }
177
218
  /**
178
219
  * Sync active chains from AccountsApiDataSource.
179
220
  * When the data source invokes the onActiveChainsUpdated callback, the
@@ -200,96 +241,40 @@ class BackendWebsocketDataSource extends AbstractDataSource_1.AbstractDataSource
200
241
  // SUBSCRIBE
201
242
  // ============================================================================
202
243
  async subscribe(subscriptionRequest) {
203
- const { request, subscriptionId, isUpdate } = subscriptionRequest;
204
- // Filter to active chains only
205
- const chainsToSubscribe = request.chainIds.filter((chainId) => this.state.activeChains.includes(chainId));
206
- const addresses = request.accountsWithSupportedChains.map((a) => a.account.address);
207
- if (addresses.length === 0) {
208
- return;
209
- }
210
- // Check WebSocket connection status
244
+ const previousLock = __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscribeLock, "f");
245
+ let releaseLock = () => undefined;
246
+ __classPrivateFieldSet(this, _BackendWebsocketDataSource_subscribeLock, new Promise((resolve) => {
247
+ releaseLock = resolve;
248
+ }), "f");
249
+ await previousLock;
211
250
  try {
212
- const connectionInfo = __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:getConnectionInfo');
213
- if (connectionInfo.state !== 'connected') {
214
- // Store the subscription request to process when WebSocket connects
215
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").set(subscriptionId, subscriptionRequest);
216
- return;
217
- }
218
- }
219
- catch {
220
- // Store anyway - will be processed when we can connect
221
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").set(subscriptionId, subscriptionRequest);
222
- return;
223
- }
224
- // Remove from pending if it was there (we're processing it now)
225
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").delete(subscriptionId);
226
- // Handle subscription update
227
- if (isUpdate) {
228
- const existing = this.activeSubscriptions.get(subscriptionId);
229
- if (existing) {
230
- // Check if accounts changed - if so, we need to re-subscribe to different channels
231
- const existingAddresses = existing.addresses ?? [];
232
- const addressesChanged = addresses.length !== existingAddresses.length ||
233
- addresses.some((addr) => !existingAddresses.includes(addr));
234
- if (!addressesChanged) {
235
- // Only chains changed - just update chains and return
236
- existing.chains = chainsToSubscribe;
237
- return;
238
- }
239
- // Accounts changed - fall through to re-subscribe with new channels
240
- }
251
+ await __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_subscribeInternal).call(this, subscriptionRequest);
241
252
  }
242
- // Clean up existing subscription if any
243
- await this.unsubscribe(subscriptionId);
244
- // Always subscribe to eip155 and solana account activity, plus any namespaces from requested chains
245
- const namespaces = getNamespacesForAccountActivity(chainsToSubscribe);
246
- // Build channel names: use namespace-appropriate address per account (eip155 = hex, solana = base58)
247
- const channels = [];
248
- for (const namespace of namespaces) {
249
- for (const { account } of request.accountsWithSupportedChains) {
250
- const address = getAddressForAccountActivity(account, namespace);
251
- if (address) {
252
- channels.push(buildAccountActivityChannel(namespace, address));
253
- }
254
- }
253
+ finally {
254
+ releaseLock();
255
255
  }
256
+ }
257
+ // ============================================================================
258
+ // UNSUBSCRIBE
259
+ // ============================================================================
260
+ /**
261
+ * Unsubscribe and await server-side teardown so a re-subscribe does not race
262
+ * with stale subscription IDs on incoming notifications.
263
+ *
264
+ * @param subscriptionId - The ID of the subscription to cancel.
265
+ */
266
+ async unsubscribe(subscriptionId) {
267
+ const previousLock = __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscribeLock, "f");
268
+ let releaseLock = () => undefined;
269
+ __classPrivateFieldSet(this, _BackendWebsocketDataSource_subscribeLock, new Promise((resolve) => {
270
+ releaseLock = resolve;
271
+ }), "f");
272
+ await previousLock;
256
273
  try {
257
- // Create WebSocket subscription
258
- const wsSubscription = await __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:subscribe', {
259
- channels,
260
- channelType: CHANNEL_TYPE,
261
- callback: (notification) => {
262
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_handleNotification).call(this, notification, subscriptionId);
263
- },
264
- });
265
- // Store WebSocket subscription
266
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").set(subscriptionId, wsSubscription);
267
- // Store in abstract class tracking
268
- this.activeSubscriptions.set(subscriptionId, {
269
- cleanup: () => {
270
- const wsSub = __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").get(subscriptionId);
271
- if (wsSub) {
272
- wsSub.unsubscribe().catch((unsubErr) => {
273
- log('Error unsubscribing', { subscriptionId, error: unsubErr });
274
- });
275
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").delete(subscriptionId);
276
- }
277
- // Also clean up the stored request
278
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").delete(subscriptionId);
279
- },
280
- chains: chainsToSubscribe,
281
- addresses,
282
- onAssetsUpdate: subscriptionRequest.onAssetsUpdate,
283
- });
284
- // Store original request for reconnection
285
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").set(subscriptionId, subscriptionRequest);
274
+ await __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_teardownSubscription).call(this, subscriptionId);
286
275
  }
287
- catch (error) {
288
- log('WebSocket subscription FAILED', {
289
- subscriptionId,
290
- error,
291
- chains: chainsToSubscribe,
292
- });
276
+ finally {
277
+ releaseLock();
293
278
  }
294
279
  }
295
280
  // ============================================================================
@@ -300,27 +285,21 @@ class BackendWebsocketDataSource extends AbstractDataSource_1.AbstractDataSource
300
285
  clearInterval(__classPrivateFieldGet(this, _BackendWebsocketDataSource_chainsRefreshTimer, "f"));
301
286
  __classPrivateFieldSet(this, _BackendWebsocketDataSource_chainsRefreshTimer, null, "f");
302
287
  }
303
- // Clean up WebSocket subscriptions
304
- // Convert to array first to avoid modifying map during iteration
305
- const subscriptions = [...__classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").values()];
306
- for (const wsSub of subscriptions) {
307
- try {
308
- // Fire and forget - don't await in destroy
309
- wsSub.unsubscribe().catch(() => {
310
- // Ignore errors during cleanup
311
- });
312
- }
313
- catch {
314
- // Ignore errors during cleanup
315
- }
288
+ const subscriptionIds = [
289
+ ...new Set([
290
+ ...__classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").keys(),
291
+ ...this.activeSubscriptions.keys(),
292
+ ]),
293
+ ];
294
+ for (const subscriptionId of subscriptionIds) {
295
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_teardownSubscription).call(this, subscriptionId).catch(() => undefined);
316
296
  }
317
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").clear();
318
- // Clean up base class subscriptions
297
+ // Clean up base class subscriptions (no-op if already torn down)
319
298
  super.destroy();
320
299
  }
321
300
  }
322
301
  exports.BackendWebsocketDataSource = BackendWebsocketDataSource;
323
- _BackendWebsocketDataSource_messenger = new WeakMap(), _BackendWebsocketDataSource_apiClient = new WeakMap(), _BackendWebsocketDataSource_onActiveChainsUpdated = new WeakMap(), _BackendWebsocketDataSource_getAssetType = new WeakMap(), _BackendWebsocketDataSource_chainsRefreshTimer = new WeakMap(), _BackendWebsocketDataSource_supportedChains = new WeakMap(), _BackendWebsocketDataSource_isConnected = new WeakMap(), _BackendWebsocketDataSource_wsSubscriptions = new WeakMap(), _BackendWebsocketDataSource_pendingSubscriptions = new WeakMap(), _BackendWebsocketDataSource_subscriptionRequests = new WeakMap(), _BackendWebsocketDataSource_instances = new WeakSet(), _BackendWebsocketDataSource_initializeActiveChains =
302
+ _BackendWebsocketDataSource_messenger = new WeakMap(), _BackendWebsocketDataSource_apiClient = new WeakMap(), _BackendWebsocketDataSource_onActiveChainsUpdated = new WeakMap(), _BackendWebsocketDataSource_getAssetType = new WeakMap(), _BackendWebsocketDataSource_chainsRefreshTimer = new WeakMap(), _BackendWebsocketDataSource_supportedChains = new WeakMap(), _BackendWebsocketDataSource_isConnected = new WeakMap(), _BackendWebsocketDataSource_wsSubscriptions = new WeakMap(), _BackendWebsocketDataSource_pendingSubscriptions = new WeakMap(), _BackendWebsocketDataSource_subscriptionRequests = new WeakMap(), _BackendWebsocketDataSource_registeredChannelCallbacks = new WeakMap(), _BackendWebsocketDataSource_subscribeLock = new WeakMap(), _BackendWebsocketDataSource_instances = new WeakSet(), _BackendWebsocketDataSource_initializeActiveChains =
324
303
  // ============================================================================
325
304
  // INITIALIZATION
326
305
  // ============================================================================
@@ -419,14 +398,111 @@ async function _BackendWebsocketDataSource_initializeActiveChains() {
419
398
  const previous = [...this.state.activeChains];
420
399
  this.updateActiveChains(__classPrivateFieldGet(this, _BackendWebsocketDataSource_supportedChains, "f"), (updatedChains) => __classPrivateFieldGet(this, _BackendWebsocketDataSource_onActiveChainsUpdated, "f").call(this, this.getName(), updatedChains, previous));
421
400
  }
401
+ }, _BackendWebsocketDataSource_subscribeInternal = async function _BackendWebsocketDataSource_subscribeInternal(subscriptionRequest) {
402
+ const { request, subscriptionId, isUpdate } = subscriptionRequest;
403
+ // Filter to active chains only
404
+ const chainsToSubscribe = request.chainIds.filter((chainId) => this.state.activeChains.includes(chainId));
405
+ const addresses = request.accountsWithSupportedChains.map((a) => a.account.address);
406
+ if (addresses.length === 0) {
407
+ return;
408
+ }
409
+ // Check WebSocket connection status
410
+ try {
411
+ const connectionInfo = __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:getConnectionInfo');
412
+ if (connectionInfo.state !== 'connected') {
413
+ // Store the subscription request to process when WebSocket connects
414
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").set(subscriptionId, subscriptionRequest);
415
+ return;
416
+ }
417
+ }
418
+ catch {
419
+ // Store anyway - will be processed when we can connect
420
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").set(subscriptionId, subscriptionRequest);
421
+ return;
422
+ }
423
+ // Remove from pending if it was there (we're processing it now)
424
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").delete(subscriptionId);
425
+ // Handle subscription update
426
+ if (isUpdate) {
427
+ const existing = this.activeSubscriptions.get(subscriptionId);
428
+ if (existing) {
429
+ // Check if accounts changed - if so, we need to re-subscribe to different channels
430
+ const existingAddresses = existing.addresses ?? [];
431
+ const addressesChanged = haveAddressesChanged(addresses, existingAddresses);
432
+ if (!addressesChanged) {
433
+ // Only chains changed - update chains, request, and callback
434
+ existing.chains = chainsToSubscribe;
435
+ existing.onAssetsUpdate = subscriptionRequest.onAssetsUpdate;
436
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").set(subscriptionId, subscriptionRequest);
437
+ return;
438
+ }
439
+ // Accounts changed - fall through to re-subscribe with new channels
440
+ }
441
+ }
442
+ // Clean up existing subscription if any (inline teardown — subscribe holds the lock)
443
+ await __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_teardownSubscription).call(this, subscriptionId);
444
+ // Always subscribe to eip155 and solana account activity, plus any namespaces from requested chains
445
+ const namespaces = getNamespacesForAccountActivity(chainsToSubscribe);
446
+ // Build channel names: use namespace-appropriate address per account (eip155 = hex, solana = base58)
447
+ const channels = [];
448
+ for (const namespace of namespaces) {
449
+ for (const { account } of request.accountsWithSupportedChains) {
450
+ const address = getAddressForAccountActivity(account, namespace);
451
+ if (address) {
452
+ channels.push(buildAccountActivityChannel(namespace, address));
453
+ }
454
+ }
455
+ }
456
+ if (channels.length === 0) {
457
+ return;
458
+ }
459
+ try {
460
+ // Register request/callback before awaiting server subscribe so notifications
461
+ // that arrive during the subscribe handshake are not dropped.
462
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").set(subscriptionId, subscriptionRequest);
463
+ this.activeSubscriptions.set(subscriptionId, {
464
+ cleanup: () => {
465
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_teardownSubscription).call(this, subscriptionId).catch(() => undefined);
466
+ },
467
+ chains: chainsToSubscribe,
468
+ addresses,
469
+ onAssetsUpdate: subscriptionRequest.onAssetsUpdate,
470
+ });
471
+ // Create WebSocket subscription
472
+ const wsSubscription = await __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:subscribe', {
473
+ channels,
474
+ channelType: CHANNEL_TYPE,
475
+ callback: (notification) => {
476
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_handleNotification).call(this, notification, subscriptionId);
477
+ },
478
+ });
479
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").set(subscriptionId, wsSubscription);
480
+ try {
481
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_registerChannelCallbacks).call(this, subscriptionId, channels);
482
+ }
483
+ catch (channelCallbackError) {
484
+ log('Channel callback registration failed; ws subscription still active', { subscriptionId, error: channelCallbackError });
485
+ }
486
+ }
487
+ catch (error) {
488
+ this.activeSubscriptions.delete(subscriptionId);
489
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").delete(subscriptionId);
490
+ log('WebSocket subscription FAILED', {
491
+ subscriptionId,
492
+ error,
493
+ chains: chainsToSubscribe,
494
+ });
495
+ }
422
496
  }, _BackendWebsocketDataSource_handleNotification = function _BackendWebsocketDataSource_handleNotification(notification, subscriptionId) {
423
497
  try {
424
- const subscription = this.activeSubscriptions.get(subscriptionId);
425
- const request = __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").get(subscriptionId)?.request;
498
+ const activityMessage = notification.data;
499
+ const storedSubscription = __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").get(subscriptionId);
500
+ const request = storedSubscription?.request;
501
+ const onAssetsUpdate = this.activeSubscriptions.get(subscriptionId)?.onAssetsUpdate ??
502
+ storedSubscription?.onAssetsUpdate;
426
503
  if (!request) {
427
504
  return;
428
505
  }
429
- const activityMessage = notification.data;
430
506
  const { address, tx, updates } = activityMessage;
431
507
  if (!address || !tx || !updates) {
432
508
  return;
@@ -445,54 +521,65 @@ async function _BackendWebsocketDataSource_initializeActiveChains() {
445
521
  const accountId = account.id;
446
522
  // Process all balance updates from the activity message
447
523
  const response = __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_processBalanceUpdates).call(this, updates, chainId, accountId);
448
- if (Object.keys(response).length > 0 && subscription) {
449
- Promise.resolve(subscription.onAssetsUpdate(response)).catch(console.error);
524
+ const balanceEntries = response.assetsBalance?.[accountId] ?? {};
525
+ const hasBalances = Object.keys(balanceEntries).length > 0;
526
+ if (hasBalances && onAssetsUpdate) {
527
+ Promise.resolve(onAssetsUpdate(response, request)).catch((error) => {
528
+ console.error(error);
529
+ });
450
530
  }
451
531
  }
452
532
  catch (error) {
453
533
  log('Error handling notification', error);
454
534
  }
455
535
  }, _BackendWebsocketDataSource_processBalanceUpdates = function _BackendWebsocketDataSource_processBalanceUpdates(updates, _chainId, accountId) {
456
- const assetsBalance = {
457
- [accountId]: {},
458
- };
459
- const assetsMetadata = {};
460
- for (const update of updates) {
461
- const { asset, postBalance } = update;
462
- if (!asset || !postBalance) {
463
- continue;
536
+ return (0, processAccountActivityBalanceUpdates_1.processAccountActivityBalanceUpdates)(updates, accountId, (assetId) => __classPrivateFieldGet(this, _BackendWebsocketDataSource_getAssetType, "f").call(this, assetId));
537
+ }, _BackendWebsocketDataSource_teardownSubscription = async function _BackendWebsocketDataSource_teardownSubscription(subscriptionId) {
538
+ const wsSub = __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").get(subscriptionId);
539
+ if (wsSub) {
540
+ const channels = [...wsSub.channels];
541
+ try {
542
+ await wsSub.unsubscribe();
464
543
  }
465
- // Asset type is in CAIP format: "eip155:1/erc20:0x..." or "eip155:1/slip44:60"
466
- // We can use it directly as the asset ID
467
- const assetId = asset.type;
468
- const tokenType = __classPrivateFieldGet(this, _BackendWebsocketDataSource_getAssetType, "f").call(this, assetId);
469
- // We assume decimals are always present; skip malformed updates
470
- if (asset.decimals === undefined) {
471
- continue;
544
+ catch (unsubErr) {
545
+ log('Error unsubscribing', { subscriptionId, error: unsubErr });
472
546
  }
473
- // Parse raw balance (hex like "0x26f0e5" or decimal string)
474
- const rawBalanceStr = postBalance.amount.startsWith('0x')
475
- ? BigInt(postBalance.amount).toString()
476
- : postBalance.amount;
477
- const humanReadableAmount = new bignumber_js_1.default(rawBalanceStr)
478
- .dividedBy(new bignumber_js_1.default(10).pow(asset.decimals))
479
- .toFixed();
480
- assetsBalance[accountId][assetId] = {
481
- amount: humanReadableAmount,
482
- };
483
- assetsMetadata[assetId] = {
484
- type: tokenType,
485
- symbol: asset.unit,
486
- name: asset.unit, // Use unit as name (actual name may not be in the message)
487
- decimals: asset.decimals,
488
- };
489
- }
490
- const response = { updateMode: 'merge' };
491
- if (Object.keys(assetsBalance[accountId]).length > 0) {
492
- response.assetsBalance = assetsBalance;
493
- response.assetsInfo = assetsMetadata;
494
- }
495
- return response;
547
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").delete(subscriptionId);
548
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_removeChannelCallbacks).call(this, channels);
549
+ }
550
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").delete(subscriptionId);
551
+ this.activeSubscriptions.delete(subscriptionId);
552
+ }, _BackendWebsocketDataSource_registerChannelCallbacks = function _BackendWebsocketDataSource_registerChannelCallbacks(subscriptionId, channels) {
553
+ for (const channel of channels) {
554
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_unregisterChannelCallback).call(this, channel);
555
+ try {
556
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:addChannelCallback', {
557
+ channelName: channel,
558
+ callback: (notification) => {
559
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_handleNotification).call(this, notification, subscriptionId);
560
+ },
561
+ });
562
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_registeredChannelCallbacks, "f").add(channel);
563
+ }
564
+ catch {
565
+ // Channel callbacks are optional; ws subscription still works without them.
566
+ }
567
+ }
568
+ }, _BackendWebsocketDataSource_unregisterChannelCallback = function _BackendWebsocketDataSource_unregisterChannelCallback(channel) {
569
+ if (!__classPrivateFieldGet(this, _BackendWebsocketDataSource_registeredChannelCallbacks, "f").has(channel)) {
570
+ return;
571
+ }
572
+ try {
573
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:removeChannelCallback', channel);
574
+ }
575
+ catch {
576
+ // Best-effort cleanup when the channel callback was never registered.
577
+ }
578
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_registeredChannelCallbacks, "f").delete(channel);
579
+ }, _BackendWebsocketDataSource_removeChannelCallbacks = function _BackendWebsocketDataSource_removeChannelCallbacks(channels) {
580
+ for (const channel of channels) {
581
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_unregisterChannelCallback).call(this, channel);
582
+ }
496
583
  };
497
584
  // ============================================================================
498
585
  // FACTORY FUNCTION