@metamask-previews/assets-controller 9.1.0-preview-50e82f088 → 9.1.0-preview-773e4e83d

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.
@@ -1 +1 @@
1
- {"version":3,"file":"BackendWebsocketDataSource.d.cts","sourceRoot":"","sources":["../../src/data-sources/BackendWebsocketDataSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,6BAA6B,EAM9B,+BAA+B;AAChC,OAAO,KAAK,EAAE,iBAAiB,EAAE,+BAA+B;AAQhE,OAAO,KAAK,EAAE,yBAAyB,EAAE,gCAA4B;AAErE,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EAId,qBAAiB;AAClB,OAAO,EAAE,kBAAkB,EAAE,iCAA6B;AAC1D,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACpB,iCAA6B;AAM9B,QAAA,MAAM,eAAe,+BAA+B,CAAC;AAUrD,MAAM,MAAM,wCAAwC,GAClD,8BAA8B,CAAC;AAGjC,MAAM,MAAM,uCAAuC,GACjD,6BAA6B,CAAC;AAMhC,MAAM,MAAM,+BAA+B,GAAG,eAAe,CAAC;AAU9D,MAAM,MAAM,iCAAiC,GAAG;IAC9C,mEAAmE;IACnE,SAAS,EAAE,yBAAyB,CAAC;IACrC,iGAAiG;IACjG,cAAc,EAAE,iBAAiB,CAAC;IAClC,qGAAqG;IACrG,qBAAqB,EAAE,CACrB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,OAAO,EAAE,EACjB,cAAc,EAAE,OAAO,EAAE,KACtB,IAAI,CAAC;IACV,wFAAwF;IACxF,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC,+BAA+B,CAAC,CAAC;CAClD,CAAC;AAqIF,qBAAa,0BAA2B,SAAQ,kBAAkB,CAChE,OAAO,eAAe,EACtB,+BAA+B,CAChC;;gBAiCa,OAAO,EAAE,iCAAiC;IAsGtD;;;;;;OAMG;IACH,8BAA8B,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IA0EvD;;;;OAIG;IACH,qBAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IAWxC,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IA+OxE,OAAO,IAAI,IAAI;CAwBhB;AAMD;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
1
+ {"version":3,"file":"BackendWebsocketDataSource.d.cts","sourceRoot":"","sources":["../../src/data-sources/BackendWebsocketDataSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,6BAA6B,EAM9B,+BAA+B;AAChC,OAAO,KAAK,EAAE,iBAAiB,EAAE,+BAA+B;AAQhE,OAAO,KAAK,EAAE,yBAAyB,EAAE,gCAA4B;AAErE,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EAId,qBAAiB;AAClB,OAAO,EAAE,kBAAkB,EAAE,iCAA6B;AAC1D,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACpB,iCAA6B;AAM9B,QAAA,MAAM,eAAe,+BAA+B,CAAC;AAUrD,MAAM,MAAM,wCAAwC,GAClD,8BAA8B,CAAC;AAGjC,MAAM,MAAM,uCAAuC,GACjD,6BAA6B,CAAC;AAMhC,MAAM,MAAM,+BAA+B,GAAG,eAAe,CAAC;AAU9D,MAAM,MAAM,iCAAiC,GAAG;IAC9C,mEAAmE;IACnE,SAAS,EAAE,yBAAyB,CAAC;IACrC,iGAAiG;IACjG,cAAc,EAAE,iBAAiB,CAAC;IAClC,qGAAqG;IACrG,qBAAqB,EAAE,CACrB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,OAAO,EAAE,EACjB,cAAc,EAAE,OAAO,EAAE,KACtB,IAAI,CAAC;IACV,wFAAwF;IACxF,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC,+BAA+B,CAAC,CAAC;CAClD,CAAC;AA4KF,qBAAa,0BAA2B,SAAQ,kBAAkB,CAChE,OAAO,eAAe,EACtB,+BAA+B,CAChC;;gBAuCa,OAAO,EAAE,iCAAiC;IAsGtD;;;;;;OAMG;IACH,8BAA8B,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IA0EvD;;;;OAIG;IACH,qBAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IAWxC,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuQxE;;;;;OAKG;IACG,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8ExD,OAAO,IAAI,IAAI;CAmBhB;AAMD;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
@@ -37,6 +37,13 @@ export declare class BackendWebsocketDataSource extends AbstractDataSource<typeo
37
37
  */
38
38
  updateSupportedChains(chains: ChainId[]): void;
39
39
  subscribe(subscriptionRequest: SubscriptionRequest): Promise<void>;
40
+ /**
41
+ * Unsubscribe and await server-side teardown so a re-subscribe does not race
42
+ * with stale subscription IDs on incoming notifications.
43
+ *
44
+ * @param subscriptionId - The ID of the subscription to cancel.
45
+ */
46
+ unsubscribe(subscriptionId: string): Promise<void>;
40
47
  destroy(): void;
41
48
  }
42
49
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"BackendWebsocketDataSource.d.mts","sourceRoot":"","sources":["../../src/data-sources/BackendWebsocketDataSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,6BAA6B,EAM9B,+BAA+B;AAChC,OAAO,KAAK,EAAE,iBAAiB,EAAE,+BAA+B;AAQhE,OAAO,KAAK,EAAE,yBAAyB,EAAE,gCAA4B;AAErE,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EAId,qBAAiB;AAClB,OAAO,EAAE,kBAAkB,EAAE,iCAA6B;AAC1D,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACpB,iCAA6B;AAM9B,QAAA,MAAM,eAAe,+BAA+B,CAAC;AAUrD,MAAM,MAAM,wCAAwC,GAClD,8BAA8B,CAAC;AAGjC,MAAM,MAAM,uCAAuC,GACjD,6BAA6B,CAAC;AAMhC,MAAM,MAAM,+BAA+B,GAAG,eAAe,CAAC;AAU9D,MAAM,MAAM,iCAAiC,GAAG;IAC9C,mEAAmE;IACnE,SAAS,EAAE,yBAAyB,CAAC;IACrC,iGAAiG;IACjG,cAAc,EAAE,iBAAiB,CAAC;IAClC,qGAAqG;IACrG,qBAAqB,EAAE,CACrB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,OAAO,EAAE,EACjB,cAAc,EAAE,OAAO,EAAE,KACtB,IAAI,CAAC;IACV,wFAAwF;IACxF,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC,+BAA+B,CAAC,CAAC;CAClD,CAAC;AAqIF,qBAAa,0BAA2B,SAAQ,kBAAkB,CAChE,OAAO,eAAe,EACtB,+BAA+B,CAChC;;gBAiCa,OAAO,EAAE,iCAAiC;IAsGtD;;;;;;OAMG;IACH,8BAA8B,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IA0EvD;;;;OAIG;IACH,qBAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IAWxC,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IA+OxE,OAAO,IAAI,IAAI;CAwBhB;AAMD;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
1
+ {"version":3,"file":"BackendWebsocketDataSource.d.mts","sourceRoot":"","sources":["../../src/data-sources/BackendWebsocketDataSource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,8BAA8B,EAC9B,6BAA6B,EAM9B,+BAA+B;AAChC,OAAO,KAAK,EAAE,iBAAiB,EAAE,+BAA+B;AAQhE,OAAO,KAAK,EAAE,yBAAyB,EAAE,gCAA4B;AAErE,OAAO,KAAK,EACV,OAAO,EACP,aAAa,EAId,qBAAiB;AAClB,OAAO,EAAE,kBAAkB,EAAE,iCAA6B;AAC1D,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACpB,iCAA6B;AAM9B,QAAA,MAAM,eAAe,+BAA+B,CAAC;AAUrD,MAAM,MAAM,wCAAwC,GAClD,8BAA8B,CAAC;AAGjC,MAAM,MAAM,uCAAuC,GACjD,6BAA6B,CAAC;AAMhC,MAAM,MAAM,+BAA+B,GAAG,eAAe,CAAC;AAU9D,MAAM,MAAM,iCAAiC,GAAG;IAC9C,mEAAmE;IACnE,SAAS,EAAE,yBAAyB,CAAC;IACrC,iGAAiG;IACjG,cAAc,EAAE,iBAAiB,CAAC;IAClC,qGAAqG;IACrG,qBAAqB,EAAE,CACrB,cAAc,EAAE,MAAM,EACtB,MAAM,EAAE,OAAO,EAAE,EACjB,cAAc,EAAE,OAAO,EAAE,KACtB,IAAI,CAAC;IACV,wFAAwF;IACxF,YAAY,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,QAAQ,GAAG,OAAO,GAAG,KAAK,CAAC;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC,+BAA+B,CAAC,CAAC;CAClD,CAAC;AA4KF,qBAAa,0BAA2B,SAAQ,kBAAkB,CAChE,OAAO,eAAe,EACtB,+BAA+B,CAChC;;gBAuCa,OAAO,EAAE,iCAAiC;IAsGtD;;;;;;OAMG;IACH,8BAA8B,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IA0EvD;;;;OAIG;IACH,qBAAqB,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI;IAWxC,SAAS,CAAC,mBAAmB,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuQxE;;;;;OAKG;IACG,WAAW,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8ExD,OAAO,IAAI,IAAI;CAmBhB;AAMD;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,iCAAiC,GACzC,0BAA0B,CAE5B"}
@@ -9,7 +9,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
9
  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");
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- 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;
12
+ 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;
13
13
  import { isCaipChainId, KnownCaipNamespace, toCaipChainId } from "@metamask/utils";
14
14
  import BigNumberJS from "bignumber.js";
15
15
  import { projectLogger, createModuleLogger } from "../logger.mjs";
@@ -89,6 +89,34 @@ function buildAccountActivityChannel(namespace, address) {
89
89
  const formatted = namespace === 'eip155' ? address.toLowerCase() : address;
90
90
  return `${CHANNEL_TYPE}.${namespace}:0:${formatted}`;
91
91
  }
92
+ /**
93
+ * Normalize addresses for stable comparison when detecting account changes.
94
+ *
95
+ * @param address - Account address (hex or base58).
96
+ * @returns Normalized address for comparison.
97
+ */
98
+ function normalizeAddressForComparison(address) {
99
+ return address.startsWith('0x') ? address.toLowerCase() : address;
100
+ }
101
+ /**
102
+ * Check whether subscribed account addresses changed (case-insensitive for EVM).
103
+ *
104
+ * @param nextAddresses - Addresses from the incoming subscribe request.
105
+ * @param existingAddresses - Addresses from the active subscription.
106
+ * @returns True when the address sets differ.
107
+ */
108
+ function haveAddressesChanged(nextAddresses, existingAddresses) {
109
+ if (nextAddresses.length !== existingAddresses.length) {
110
+ return true;
111
+ }
112
+ const normalizedNext = nextAddresses
113
+ .map(normalizeAddressForComparison)
114
+ .sort();
115
+ const normalizedExisting = existingAddresses
116
+ .map(normalizeAddressForComparison)
117
+ .sort();
118
+ return normalizedNext.some((address, index) => address !== normalizedExisting[index]);
119
+ }
92
120
  /**
93
121
  * Normalize API chain identifier to CAIP-2 ChainId.
94
122
  * Passes through strings already in CAIP-2 form (e.g. eip155:1, solana:5eykt...).
@@ -136,6 +164,8 @@ function toChainId(chainIdOrDecimal) {
136
164
  * - BackendWebSocketService:subscribe
137
165
  * - BackendWebSocketService:getConnectionInfo
138
166
  * - BackendWebSocketService:findSubscriptionsByChannelPrefix
167
+ * - BackendWebSocketService:addChannelCallback
168
+ * - BackendWebSocketService:removeChannelCallback
139
169
  */
140
170
  const DEFAULT_CHAINS_REFRESH_INTERVAL_MS = 20 * 60 * 1000; // 20 minutes
141
171
  export class BackendWebsocketDataSource extends AbstractDataSource {
@@ -161,6 +191,10 @@ export class BackendWebsocketDataSource extends AbstractDataSource {
161
191
  _BackendWebsocketDataSource_pendingSubscriptions.set(this, new Map());
162
192
  /** Store original subscription requests for reconnection */
163
193
  _BackendWebsocketDataSource_subscriptionRequests.set(this, new Map());
194
+ /** Channels with registered BackendWebSocketService channel callbacks */
195
+ _BackendWebsocketDataSource_registeredChannelCallbacks.set(this, new Set());
196
+ /** Serializes subscribe/unsubscribe so account switches cannot interleave. */
197
+ _BackendWebsocketDataSource_subscribeLock.set(this, Promise.resolve());
164
198
  __classPrivateFieldSet(this, _BackendWebsocketDataSource_messenger, options.messenger, "f");
165
199
  __classPrivateFieldSet(this, _BackendWebsocketDataSource_apiClient, options.queryApiClient, "f");
166
200
  __classPrivateFieldSet(this, _BackendWebsocketDataSource_onActiveChainsUpdated, options.onActiveChainsUpdated, "f");
@@ -194,96 +228,40 @@ export class BackendWebsocketDataSource extends AbstractDataSource {
194
228
  // SUBSCRIBE
195
229
  // ============================================================================
196
230
  async subscribe(subscriptionRequest) {
197
- const { request, subscriptionId, isUpdate } = subscriptionRequest;
198
- // Filter to active chains only
199
- const chainsToSubscribe = request.chainIds.filter((chainId) => this.state.activeChains.includes(chainId));
200
- const addresses = request.accountsWithSupportedChains.map((a) => a.account.address);
201
- if (addresses.length === 0) {
202
- return;
203
- }
204
- // Check WebSocket connection status
231
+ const previousLock = __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscribeLock, "f");
232
+ let releaseLock = () => undefined;
233
+ __classPrivateFieldSet(this, _BackendWebsocketDataSource_subscribeLock, new Promise((resolve) => {
234
+ releaseLock = resolve;
235
+ }), "f");
236
+ await previousLock;
205
237
  try {
206
- const connectionInfo = __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:getConnectionInfo');
207
- if (connectionInfo.state !== 'connected') {
208
- // Store the subscription request to process when WebSocket connects
209
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").set(subscriptionId, subscriptionRequest);
210
- return;
211
- }
212
- }
213
- catch {
214
- // Store anyway - will be processed when we can connect
215
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").set(subscriptionId, subscriptionRequest);
216
- return;
238
+ await __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_subscribeInternal).call(this, subscriptionRequest);
217
239
  }
218
- // Remove from pending if it was there (we're processing it now)
219
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").delete(subscriptionId);
220
- // Handle subscription update
221
- if (isUpdate) {
222
- const existing = this.activeSubscriptions.get(subscriptionId);
223
- if (existing) {
224
- // Check if accounts changed - if so, we need to re-subscribe to different channels
225
- const existingAddresses = existing.addresses ?? [];
226
- const addressesChanged = addresses.length !== existingAddresses.length ||
227
- addresses.some((addr) => !existingAddresses.includes(addr));
228
- if (!addressesChanged) {
229
- // Only chains changed - just update chains and return
230
- existing.chains = chainsToSubscribe;
231
- return;
232
- }
233
- // Accounts changed - fall through to re-subscribe with new channels
234
- }
235
- }
236
- // Clean up existing subscription if any
237
- await this.unsubscribe(subscriptionId);
238
- // Always subscribe to eip155 and solana account activity, plus any namespaces from requested chains
239
- const namespaces = getNamespacesForAccountActivity(chainsToSubscribe);
240
- // Build channel names: use namespace-appropriate address per account (eip155 = hex, solana = base58)
241
- const channels = [];
242
- for (const namespace of namespaces) {
243
- for (const { account } of request.accountsWithSupportedChains) {
244
- const address = getAddressForAccountActivity(account, namespace);
245
- if (address) {
246
- channels.push(buildAccountActivityChannel(namespace, address));
247
- }
248
- }
240
+ finally {
241
+ releaseLock();
249
242
  }
243
+ }
244
+ // ============================================================================
245
+ // UNSUBSCRIBE
246
+ // ============================================================================
247
+ /**
248
+ * Unsubscribe and await server-side teardown so a re-subscribe does not race
249
+ * with stale subscription IDs on incoming notifications.
250
+ *
251
+ * @param subscriptionId - The ID of the subscription to cancel.
252
+ */
253
+ async unsubscribe(subscriptionId) {
254
+ const previousLock = __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscribeLock, "f");
255
+ let releaseLock = () => undefined;
256
+ __classPrivateFieldSet(this, _BackendWebsocketDataSource_subscribeLock, new Promise((resolve) => {
257
+ releaseLock = resolve;
258
+ }), "f");
259
+ await previousLock;
250
260
  try {
251
- // Create WebSocket subscription
252
- const wsSubscription = await __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:subscribe', {
253
- channels,
254
- channelType: CHANNEL_TYPE,
255
- callback: (notification) => {
256
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_handleNotification).call(this, notification, subscriptionId);
257
- },
258
- });
259
- // Store WebSocket subscription
260
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").set(subscriptionId, wsSubscription);
261
- // Store in abstract class tracking
262
- this.activeSubscriptions.set(subscriptionId, {
263
- cleanup: () => {
264
- const wsSub = __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").get(subscriptionId);
265
- if (wsSub) {
266
- wsSub.unsubscribe().catch((unsubErr) => {
267
- log('Error unsubscribing', { subscriptionId, error: unsubErr });
268
- });
269
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").delete(subscriptionId);
270
- }
271
- // Also clean up the stored request
272
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").delete(subscriptionId);
273
- },
274
- chains: chainsToSubscribe,
275
- addresses,
276
- onAssetsUpdate: subscriptionRequest.onAssetsUpdate,
277
- });
278
- // Store original request for reconnection
279
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").set(subscriptionId, subscriptionRequest);
261
+ await __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_teardownSubscription).call(this, subscriptionId);
280
262
  }
281
- catch (error) {
282
- log('WebSocket subscription FAILED', {
283
- subscriptionId,
284
- error,
285
- chains: chainsToSubscribe,
286
- });
263
+ finally {
264
+ releaseLock();
287
265
  }
288
266
  }
289
267
  // ============================================================================
@@ -294,26 +272,20 @@ export class BackendWebsocketDataSource extends AbstractDataSource {
294
272
  clearInterval(__classPrivateFieldGet(this, _BackendWebsocketDataSource_chainsRefreshTimer, "f"));
295
273
  __classPrivateFieldSet(this, _BackendWebsocketDataSource_chainsRefreshTimer, null, "f");
296
274
  }
297
- // Clean up WebSocket subscriptions
298
- // Convert to array first to avoid modifying map during iteration
299
- const subscriptions = [...__classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").values()];
300
- for (const wsSub of subscriptions) {
301
- try {
302
- // Fire and forget - don't await in destroy
303
- wsSub.unsubscribe().catch(() => {
304
- // Ignore errors during cleanup
305
- });
306
- }
307
- catch {
308
- // Ignore errors during cleanup
309
- }
275
+ const subscriptionIds = [
276
+ ...new Set([
277
+ ...__classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").keys(),
278
+ ...this.activeSubscriptions.keys(),
279
+ ]),
280
+ ];
281
+ for (const subscriptionId of subscriptionIds) {
282
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_teardownSubscription).call(this, subscriptionId).catch(() => undefined);
310
283
  }
311
- __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").clear();
312
- // Clean up base class subscriptions
284
+ // Clean up base class subscriptions (no-op if already torn down)
313
285
  super.destroy();
314
286
  }
315
287
  }
316
- _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 =
288
+ _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 =
317
289
  // ============================================================================
318
290
  // INITIALIZATION
319
291
  // ============================================================================
@@ -412,6 +384,99 @@ async function _BackendWebsocketDataSource_initializeActiveChains() {
412
384
  const previous = [...this.state.activeChains];
413
385
  this.updateActiveChains(__classPrivateFieldGet(this, _BackendWebsocketDataSource_supportedChains, "f"), (updatedChains) => __classPrivateFieldGet(this, _BackendWebsocketDataSource_onActiveChainsUpdated, "f").call(this, this.getName(), updatedChains, previous));
414
386
  }
387
+ }, _BackendWebsocketDataSource_subscribeInternal = async function _BackendWebsocketDataSource_subscribeInternal(subscriptionRequest) {
388
+ const { request, subscriptionId, isUpdate } = subscriptionRequest;
389
+ // Filter to active chains only
390
+ const chainsToSubscribe = request.chainIds.filter((chainId) => this.state.activeChains.includes(chainId));
391
+ const addresses = request.accountsWithSupportedChains.map((a) => a.account.address);
392
+ if (addresses.length === 0) {
393
+ return;
394
+ }
395
+ // Check WebSocket connection status
396
+ try {
397
+ const connectionInfo = __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:getConnectionInfo');
398
+ if (connectionInfo.state !== 'connected') {
399
+ // Store the subscription request to process when WebSocket connects
400
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").set(subscriptionId, subscriptionRequest);
401
+ return;
402
+ }
403
+ }
404
+ catch {
405
+ // Store anyway - will be processed when we can connect
406
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").set(subscriptionId, subscriptionRequest);
407
+ return;
408
+ }
409
+ // Remove from pending if it was there (we're processing it now)
410
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_pendingSubscriptions, "f").delete(subscriptionId);
411
+ // Handle subscription update
412
+ if (isUpdate) {
413
+ const existing = this.activeSubscriptions.get(subscriptionId);
414
+ if (existing) {
415
+ // Check if accounts changed - if so, we need to re-subscribe to different channels
416
+ const existingAddresses = existing.addresses ?? [];
417
+ const addressesChanged = haveAddressesChanged(addresses, existingAddresses);
418
+ if (!addressesChanged) {
419
+ // Only chains changed - update chains, request, and callback
420
+ existing.chains = chainsToSubscribe;
421
+ existing.onAssetsUpdate = subscriptionRequest.onAssetsUpdate;
422
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").set(subscriptionId, subscriptionRequest);
423
+ return;
424
+ }
425
+ // Accounts changed - fall through to re-subscribe with new channels
426
+ }
427
+ }
428
+ // Clean up existing subscription if any (inline teardown — subscribe holds the lock)
429
+ await __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_teardownSubscription).call(this, subscriptionId);
430
+ // Always subscribe to eip155 and solana account activity, plus any namespaces from requested chains
431
+ const namespaces = getNamespacesForAccountActivity(chainsToSubscribe);
432
+ // Build channel names: use namespace-appropriate address per account (eip155 = hex, solana = base58)
433
+ const channels = [];
434
+ for (const namespace of namespaces) {
435
+ for (const { account } of request.accountsWithSupportedChains) {
436
+ const address = getAddressForAccountActivity(account, namespace);
437
+ if (address) {
438
+ channels.push(buildAccountActivityChannel(namespace, address));
439
+ }
440
+ }
441
+ }
442
+ if (channels.length === 0) {
443
+ return;
444
+ }
445
+ try {
446
+ // Create WebSocket subscription
447
+ const wsSubscription = await __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:subscribe', {
448
+ channels,
449
+ channelType: CHANNEL_TYPE,
450
+ callback: (notification) => {
451
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_handleNotification).call(this, notification, subscriptionId);
452
+ },
453
+ });
454
+ // Store WebSocket subscription and subscription state before optional
455
+ // channel callbacks — wsCallback routing works without them.
456
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").set(subscriptionId, wsSubscription);
457
+ this.activeSubscriptions.set(subscriptionId, {
458
+ cleanup: () => {
459
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_teardownSubscription).call(this, subscriptionId).catch(() => undefined);
460
+ },
461
+ chains: chainsToSubscribe,
462
+ addresses,
463
+ onAssetsUpdate: subscriptionRequest.onAssetsUpdate,
464
+ });
465
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").set(subscriptionId, subscriptionRequest);
466
+ try {
467
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_registerChannelCallbacks).call(this, subscriptionId, channels);
468
+ }
469
+ catch (channelCallbackError) {
470
+ log('Channel callback registration failed; ws subscription still active', { subscriptionId, error: channelCallbackError });
471
+ }
472
+ }
473
+ catch (error) {
474
+ log('WebSocket subscription FAILED', {
475
+ subscriptionId,
476
+ error,
477
+ chains: chainsToSubscribe,
478
+ });
479
+ }
415
480
  }, _BackendWebsocketDataSource_handleNotification = function _BackendWebsocketDataSource_handleNotification(notification, subscriptionId) {
416
481
  try {
417
482
  const subscription = this.activeSubscriptions.get(subscriptionId);
@@ -439,7 +504,7 @@ async function _BackendWebsocketDataSource_initializeActiveChains() {
439
504
  // Process all balance updates from the activity message
440
505
  const response = __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_processBalanceUpdates).call(this, updates, chainId, accountId);
441
506
  if (Object.keys(response).length > 0 && subscription) {
442
- Promise.resolve(subscription.onAssetsUpdate(response)).catch(console.error);
507
+ Promise.resolve(subscription.onAssetsUpdate(response, request)).catch(console.error);
443
508
  }
444
509
  }
445
510
  catch (error) {
@@ -486,6 +551,52 @@ async function _BackendWebsocketDataSource_initializeActiveChains() {
486
551
  response.assetsInfo = assetsMetadata;
487
552
  }
488
553
  return response;
554
+ }, _BackendWebsocketDataSource_teardownSubscription = async function _BackendWebsocketDataSource_teardownSubscription(subscriptionId) {
555
+ const wsSub = __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").get(subscriptionId);
556
+ if (wsSub) {
557
+ const channels = [...wsSub.channels];
558
+ try {
559
+ await wsSub.unsubscribe();
560
+ }
561
+ catch (unsubErr) {
562
+ log('Error unsubscribing', { subscriptionId, error: unsubErr });
563
+ }
564
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_wsSubscriptions, "f").delete(subscriptionId);
565
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_removeChannelCallbacks).call(this, channels);
566
+ }
567
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_subscriptionRequests, "f").delete(subscriptionId);
568
+ this.activeSubscriptions.delete(subscriptionId);
569
+ }, _BackendWebsocketDataSource_registerChannelCallbacks = function _BackendWebsocketDataSource_registerChannelCallbacks(subscriptionId, channels) {
570
+ for (const channel of channels) {
571
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_unregisterChannelCallback).call(this, channel);
572
+ try {
573
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:addChannelCallback', {
574
+ channelName: channel,
575
+ callback: (notification) => {
576
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_handleNotification).call(this, notification, subscriptionId);
577
+ },
578
+ });
579
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_registeredChannelCallbacks, "f").add(channel);
580
+ }
581
+ catch {
582
+ // Channel callbacks are optional; ws subscription still works without them.
583
+ }
584
+ }
585
+ }, _BackendWebsocketDataSource_unregisterChannelCallback = function _BackendWebsocketDataSource_unregisterChannelCallback(channel) {
586
+ if (!__classPrivateFieldGet(this, _BackendWebsocketDataSource_registeredChannelCallbacks, "f").has(channel)) {
587
+ return;
588
+ }
589
+ try {
590
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_messenger, "f").call('BackendWebSocketService:removeChannelCallback', channel);
591
+ }
592
+ catch {
593
+ // Best-effort cleanup when the channel callback was never registered.
594
+ }
595
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_registeredChannelCallbacks, "f").delete(channel);
596
+ }, _BackendWebsocketDataSource_removeChannelCallbacks = function _BackendWebsocketDataSource_removeChannelCallbacks(channels) {
597
+ for (const channel of channels) {
598
+ __classPrivateFieldGet(this, _BackendWebsocketDataSource_instances, "m", _BackendWebsocketDataSource_unregisterChannelCallback).call(this, channel);
599
+ }
489
600
  };
490
601
  // ============================================================================
491
602
  // FACTORY FUNCTION