@metamask-previews/core-backend 1.0.1-preview-0189b42 → 1.0.1-preview-5cf3ff79
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.
- package/CHANGELOG.md +0 -30
- package/README.md +22 -62
- package/dist/AccountActivityService.cjs +88 -37
- package/dist/AccountActivityService.cjs.map +1 -1
- package/dist/AccountActivityService.d.cts +7 -3
- package/dist/AccountActivityService.d.cts.map +1 -1
- package/dist/AccountActivityService.d.mts +7 -3
- package/dist/AccountActivityService.d.mts.map +1 -1
- package/dist/AccountActivityService.mjs +88 -37
- package/dist/AccountActivityService.mjs.map +1 -1
- package/dist/BackendWebSocketService.cjs +96 -175
- package/dist/BackendWebSocketService.cjs.map +1 -1
- package/dist/BackendWebSocketService.d.cts +12 -31
- package/dist/BackendWebSocketService.d.cts.map +1 -1
- package/dist/BackendWebSocketService.d.mts +12 -31
- package/dist/BackendWebSocketService.d.mts.map +1 -1
- package/dist/BackendWebSocketService.mjs +96 -175
- package/dist/BackendWebSocketService.mjs.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +2 -4
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +2 -4
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +3 -4
|
@@ -15,13 +15,50 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
|
|
|
15
15
|
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");
|
|
16
16
|
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
17
17
|
};
|
|
18
|
-
var _AccountActivityService_instances, _AccountActivityService_messenger, _AccountActivityService_options,
|
|
18
|
+
var _AccountActivityService_instances, _AccountActivityService_messenger, _AccountActivityService_options, _AccountActivityService_supportedChains, _AccountActivityService_supportedChainsExpiresAt, _AccountActivityService_handleAccountActivityUpdate, _AccountActivityService_handleSelectedAccountChange, _AccountActivityService_handleSystemNotification, _AccountActivityService_handleWebSocketStateChange, _AccountActivityService_subscribeToSelectedAccount, _AccountActivityService_unsubscribeFromAllAccountActivity, _AccountActivityService_convertToCaip10Address, _AccountActivityService_forceReconnection;
|
|
19
19
|
import { WebSocketState } from "./BackendWebSocketService.mjs";
|
|
20
20
|
import { projectLogger, createModuleLogger } from "./logger.mjs";
|
|
21
|
+
// =============================================================================
|
|
22
|
+
// Utility Functions
|
|
23
|
+
// =============================================================================
|
|
24
|
+
/**
|
|
25
|
+
* Fetches supported networks from the v2 API endpoint.
|
|
26
|
+
* Returns chain IDs already in CAIP-2 format.
|
|
27
|
+
*
|
|
28
|
+
* Note: This directly calls the Account API v2 endpoint. In the future, this should
|
|
29
|
+
* be moved to a dedicated data layer service for better separation of concerns.
|
|
30
|
+
*
|
|
31
|
+
* @returns Array of supported chain IDs in CAIP-2 format (e.g., "eip155:1")
|
|
32
|
+
*/
|
|
33
|
+
async function fetchSupportedChainsInCaipFormat() {
|
|
34
|
+
const url = 'https://accounts.api.cx.metamask.io/v2/supportedNetworks';
|
|
35
|
+
const response = await fetch(url);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
throw new Error(`Failed to fetch supported networks: ${response.status} ${response.statusText}`);
|
|
38
|
+
}
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
// v2 endpoint already returns data in CAIP-2 format
|
|
41
|
+
return data.fullSupport;
|
|
42
|
+
}
|
|
21
43
|
const SERVICE_NAME = 'AccountActivityService';
|
|
22
44
|
const log = createModuleLogger(projectLogger, SERVICE_NAME);
|
|
23
45
|
const MESSENGER_EXPOSED_METHODS = ['subscribe', 'unsubscribe'];
|
|
46
|
+
// Default supported chains used as fallback when API is unavailable
|
|
47
|
+
// This list should match the expected chains from the accounts API v2/supportedNetworks endpoint
|
|
48
|
+
const DEFAULT_SUPPORTED_CHAINS = [
|
|
49
|
+
'eip155:1',
|
|
50
|
+
'eip155:137',
|
|
51
|
+
'eip155:56',
|
|
52
|
+
'eip155:59144',
|
|
53
|
+
'eip155:8453',
|
|
54
|
+
'eip155:10',
|
|
55
|
+
'eip155:42161',
|
|
56
|
+
'eip155:534352',
|
|
57
|
+
'eip155:1329', // Sei
|
|
58
|
+
];
|
|
24
59
|
const SUBSCRIPTION_NAMESPACE = 'account-activity.v1';
|
|
60
|
+
// Cache TTL for supported chains (5 hours in milliseconds)
|
|
61
|
+
const SUPPORTED_CHAINS_CACHE_TTL = 5 * 60 * 60 * 1000;
|
|
25
62
|
// Allowed actions that AccountActivityService can call on other controllers
|
|
26
63
|
export const ACCOUNT_ACTIVITY_SERVICE_ALLOWED_ACTIONS = [
|
|
27
64
|
'AccountsController:getSelectedAccount',
|
|
@@ -94,8 +131,8 @@ export class AccountActivityService {
|
|
|
94
131
|
this.name = SERVICE_NAME;
|
|
95
132
|
_AccountActivityService_messenger.set(this, void 0);
|
|
96
133
|
_AccountActivityService_options.set(this, void 0);
|
|
97
|
-
|
|
98
|
-
|
|
134
|
+
_AccountActivityService_supportedChains.set(this, null);
|
|
135
|
+
_AccountActivityService_supportedChainsExpiresAt.set(this, 0);
|
|
99
136
|
__classPrivateFieldSet(this, _AccountActivityService_messenger, options.messenger, "f");
|
|
100
137
|
// Set configuration with defaults
|
|
101
138
|
__classPrivateFieldSet(this, _AccountActivityService_options, {
|
|
@@ -106,10 +143,36 @@ export class AccountActivityService {
|
|
|
106
143
|
__classPrivateFieldGet(this, _AccountActivityService_messenger, "f").subscribe('BackendWebSocketService:connectionStateChanged', (connectionInfo) => __classPrivateFieldGet(this, _AccountActivityService_instances, "m", _AccountActivityService_handleWebSocketStateChange).call(this, connectionInfo));
|
|
107
144
|
__classPrivateFieldGet(this, _AccountActivityService_messenger, "f").call('BackendWebSocketService:addChannelCallback', {
|
|
108
145
|
channelName: `system-notifications.v1.${__classPrivateFieldGet(this, _AccountActivityService_options, "f").subscriptionNamespace}`,
|
|
109
|
-
callback: (notification) => __classPrivateFieldGet(this, _AccountActivityService_instances, "m", _AccountActivityService_handleSystemNotification).call(this, notification),
|
|
146
|
+
callback: (notification) => __classPrivateFieldGet(this, _AccountActivityService_instances, "m", _AccountActivityService_handleSystemNotification).call(this, notification.data),
|
|
110
147
|
});
|
|
111
148
|
}
|
|
112
149
|
// =============================================================================
|
|
150
|
+
// Public Methods - Chain Management
|
|
151
|
+
// =============================================================================
|
|
152
|
+
/**
|
|
153
|
+
* Fetch supported chains from API with fallback to hardcoded list.
|
|
154
|
+
* Uses expiry-based caching with TTL to prevent stale data.
|
|
155
|
+
*
|
|
156
|
+
* @returns Array of supported chain IDs in CAIP-2 format
|
|
157
|
+
*/
|
|
158
|
+
async getSupportedChains() {
|
|
159
|
+
// Return cached result if available and not expired
|
|
160
|
+
if (__classPrivateFieldGet(this, _AccountActivityService_supportedChains, "f") !== null &&
|
|
161
|
+
Date.now() < __classPrivateFieldGet(this, _AccountActivityService_supportedChainsExpiresAt, "f")) {
|
|
162
|
+
return __classPrivateFieldGet(this, _AccountActivityService_supportedChains, "f");
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
// Try to fetch from API
|
|
166
|
+
__classPrivateFieldSet(this, _AccountActivityService_supportedChains, await fetchSupportedChainsInCaipFormat(), "f");
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Fallback to hardcoded list and cache it with timestamp
|
|
170
|
+
__classPrivateFieldSet(this, _AccountActivityService_supportedChains, Array.from(DEFAULT_SUPPORTED_CHAINS), "f");
|
|
171
|
+
}
|
|
172
|
+
__classPrivateFieldSet(this, _AccountActivityService_supportedChainsExpiresAt, Date.now() + SUPPORTED_CHAINS_CACHE_TTL, "f");
|
|
173
|
+
return __classPrivateFieldGet(this, _AccountActivityService_supportedChains, "f");
|
|
174
|
+
}
|
|
175
|
+
// =============================================================================
|
|
113
176
|
// Account Subscription Methods
|
|
114
177
|
// =============================================================================
|
|
115
178
|
/**
|
|
@@ -130,7 +193,6 @@ export class AccountActivityService {
|
|
|
130
193
|
// Create subscription using the proper subscribe method (this will be stored in WebSocketService's internal tracking)
|
|
131
194
|
await __classPrivateFieldGet(this, _AccountActivityService_messenger, "f").call('BackendWebSocketService:subscribe', {
|
|
132
195
|
channels: [channel],
|
|
133
|
-
channelType: __classPrivateFieldGet(this, _AccountActivityService_options, "f").subscriptionNamespace,
|
|
134
196
|
callback: (notification) => {
|
|
135
197
|
__classPrivateFieldGet(this, _AccountActivityService_instances, "m", _AccountActivityService_handleAccountActivityUpdate).call(this, notification.data);
|
|
136
198
|
},
|
|
@@ -179,7 +241,7 @@ export class AccountActivityService {
|
|
|
179
241
|
__classPrivateFieldGet(this, _AccountActivityService_messenger, "f").call('BackendWebSocketService:removeChannelCallback', `system-notifications.v1.${__classPrivateFieldGet(this, _AccountActivityService_options, "f").subscriptionNamespace}`);
|
|
180
242
|
}
|
|
181
243
|
}
|
|
182
|
-
_AccountActivityService_messenger = new WeakMap(), _AccountActivityService_options = new WeakMap(),
|
|
244
|
+
_AccountActivityService_messenger = new WeakMap(), _AccountActivityService_options = new WeakMap(), _AccountActivityService_supportedChains = new WeakMap(), _AccountActivityService_supportedChainsExpiresAt = new WeakMap(), _AccountActivityService_instances = new WeakSet(), _AccountActivityService_handleAccountActivityUpdate = function _AccountActivityService_handleAccountActivityUpdate(payload) {
|
|
183
245
|
const { address, tx, updates } = payload;
|
|
184
246
|
log('Handling account activity update', {
|
|
185
247
|
address,
|
|
@@ -214,29 +276,15 @@ async function _AccountActivityService_handleSelectedAccountChange(newAccount) {
|
|
|
214
276
|
catch (error) {
|
|
215
277
|
log('Account change failed', { error });
|
|
216
278
|
}
|
|
217
|
-
}, _AccountActivityService_handleSystemNotification = function _AccountActivityService_handleSystemNotification(
|
|
218
|
-
const data = notification.data;
|
|
219
|
-
const { timestamp } = notification;
|
|
279
|
+
}, _AccountActivityService_handleSystemNotification = function _AccountActivityService_handleSystemNotification(data) {
|
|
220
280
|
// Validate required fields
|
|
221
281
|
if (!data.chainIds || !Array.isArray(data.chainIds) || !data.status) {
|
|
222
282
|
throw new Error('Invalid system notification data: missing chainIds or status');
|
|
223
283
|
}
|
|
224
|
-
// Track chain status
|
|
225
|
-
if (data.status === 'up') {
|
|
226
|
-
for (const chainId of data.chainIds) {
|
|
227
|
-
__classPrivateFieldGet(this, _AccountActivityService_chainsUp, "f").add(chainId);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
else {
|
|
231
|
-
for (const chainId of data.chainIds) {
|
|
232
|
-
__classPrivateFieldGet(this, _AccountActivityService_chainsUp, "f").delete(chainId);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
284
|
// Publish status change directly (delta update)
|
|
236
285
|
__classPrivateFieldGet(this, _AccountActivityService_messenger, "f").publish(`AccountActivityService:statusChanged`, {
|
|
237
286
|
chainIds: data.chainIds,
|
|
238
287
|
status: data.status,
|
|
239
|
-
timestamp,
|
|
240
288
|
});
|
|
241
289
|
}, _AccountActivityService_handleWebSocketStateChange =
|
|
242
290
|
/**
|
|
@@ -246,27 +294,30 @@ async function _AccountActivityService_handleSelectedAccountChange(newAccount) {
|
|
|
246
294
|
*/
|
|
247
295
|
async function _AccountActivityService_handleWebSocketStateChange(connectionInfo) {
|
|
248
296
|
const { state } = connectionInfo;
|
|
297
|
+
const supportedChains = await this.getSupportedChains();
|
|
249
298
|
if (state === WebSocketState.CONNECTED) {
|
|
250
|
-
// WebSocket connected - resubscribe
|
|
251
|
-
// The system notification will automatically provide the list of chains that are up
|
|
299
|
+
// WebSocket connected - resubscribe and set all chains as up
|
|
252
300
|
await __classPrivateFieldGet(this, _AccountActivityService_instances, "m", _AccountActivityService_subscribeToSelectedAccount).call(this);
|
|
301
|
+
// Publish initial status - all supported chains are up when WebSocket connects
|
|
302
|
+
__classPrivateFieldGet(this, _AccountActivityService_messenger, "f").publish(`AccountActivityService:statusChanged`, {
|
|
303
|
+
chainIds: supportedChains,
|
|
304
|
+
status: 'up',
|
|
305
|
+
});
|
|
306
|
+
log('WebSocket connected - Published all chains as up', {
|
|
307
|
+
count: supportedChains.length,
|
|
308
|
+
chains: supportedChains,
|
|
309
|
+
});
|
|
253
310
|
}
|
|
254
311
|
else if (state === WebSocketState.DISCONNECTED ||
|
|
255
312
|
state === WebSocketState.ERROR) {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
count: chainsToMarkDown.length,
|
|
265
|
-
chains: chainsToMarkDown,
|
|
266
|
-
});
|
|
267
|
-
// Clear the tracking set since all chains are now down
|
|
268
|
-
__classPrivateFieldGet(this, _AccountActivityService_chainsUp, "f").clear();
|
|
269
|
-
}
|
|
313
|
+
__classPrivateFieldGet(this, _AccountActivityService_messenger, "f").publish(`AccountActivityService:statusChanged`, {
|
|
314
|
+
chainIds: supportedChains,
|
|
315
|
+
status: 'down',
|
|
316
|
+
});
|
|
317
|
+
log('WebSocket error/disconnection - Published all chains as down', {
|
|
318
|
+
count: supportedChains.length,
|
|
319
|
+
chains: supportedChains,
|
|
320
|
+
});
|
|
270
321
|
}
|
|
271
322
|
}, _AccountActivityService_subscribeToSelectedAccount =
|
|
272
323
|
// =============================================================================
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AccountActivityService.mjs","sourceRoot":"","sources":["../src/AccountActivityService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Account Activity Service for monitoring account transactions and balance changes\n *\n * This service subscribes to account activity and receives all transactions\n * and balance updates for those accounts via the comprehensive AccountActivityMessage format.\n */\n\nimport type {\n AccountsControllerGetSelectedAccountAction,\n AccountsControllerSelectedAccountChangeEvent,\n} from '@metamask/accounts-controller';\nimport type { RestrictedMessenger } from '@metamask/base-controller';\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\n\nimport type { AccountActivityServiceMethodActions } from './AccountActivityService-method-action-types';\nimport type {\n WebSocketConnectionInfo,\n BackendWebSocketServiceConnectionStateChangedEvent,\n ServerNotificationMessage,\n} from './BackendWebSocketService';\nimport { WebSocketState } from './BackendWebSocketService';\nimport type { BackendWebSocketServiceMethodActions } from './BackendWebSocketService-method-action-types';\nimport { projectLogger, createModuleLogger } from './logger';\nimport type {\n Transaction,\n AccountActivityMessage,\n BalanceUpdate,\n} from './types';\n\n// =============================================================================\n// Types and Constants\n// =============================================================================\n\n/**\n * System notification data for chain status updates\n */\nexport type SystemNotificationData = {\n /** Array of chain IDs affected (e.g., ['eip155:137', 'eip155:1']) */\n chainIds: string[];\n /** Status of the chains: 'down' or 'up' */\n status: 'down' | 'up';\n /** Timestamp of the notification */\n timestamp?: number;\n};\n\nconst SERVICE_NAME = 'AccountActivityService';\n\nconst log = createModuleLogger(projectLogger, SERVICE_NAME);\n\nconst MESSENGER_EXPOSED_METHODS = ['subscribe', 'unsubscribe'] as const;\n\nconst SUBSCRIPTION_NAMESPACE = 'account-activity.v1';\n\n/**\n * Account subscription options\n */\nexport type SubscriptionOptions = {\n address: string; // Should be in CAIP-10 format, e.g., \"eip155:0:0x1234...\" or \"solana:0:ABC123...\"\n};\n\n/**\n * Configuration options for the account activity service\n */\nexport type AccountActivityServiceOptions = {\n /** Custom subscription namespace (default: 'account-activity.v1') */\n subscriptionNamespace?: string;\n};\n\n// =============================================================================\n// Action and Event Types\n// =============================================================================\n\n// Action types for the messaging system - using generated method actions\nexport type AccountActivityServiceActions = AccountActivityServiceMethodActions;\n\n// Allowed actions that AccountActivityService can call on other controllers\nexport const ACCOUNT_ACTIVITY_SERVICE_ALLOWED_ACTIONS = [\n 'AccountsController:getSelectedAccount',\n 'BackendWebSocketService:connect',\n 'BackendWebSocketService:disconnect',\n 'BackendWebSocketService:subscribe',\n 'BackendWebSocketService:getConnectionInfo',\n 'BackendWebSocketService:channelHasSubscription',\n 'BackendWebSocketService:getSubscriptionsByChannel',\n 'BackendWebSocketService:findSubscriptionsByChannelPrefix',\n 'BackendWebSocketService:addChannelCallback',\n 'BackendWebSocketService:removeChannelCallback',\n] as const;\n\n// Allowed events that AccountActivityService can listen to\nexport const ACCOUNT_ACTIVITY_SERVICE_ALLOWED_EVENTS = [\n 'AccountsController:selectedAccountChange',\n 'BackendWebSocketService:connectionStateChanged',\n] as const;\n\nexport type AccountActivityServiceAllowedActions =\n | AccountsControllerGetSelectedAccountAction\n | BackendWebSocketServiceMethodActions;\n\n// Event types for the messaging system\n\nexport type AccountActivityServiceTransactionUpdatedEvent = {\n type: `AccountActivityService:transactionUpdated`;\n payload: [Transaction];\n};\n\nexport type AccountActivityServiceBalanceUpdatedEvent = {\n type: `AccountActivityService:balanceUpdated`;\n payload: [{ address: string; chain: string; updates: BalanceUpdate[] }];\n};\n\nexport type AccountActivityServiceSubscriptionErrorEvent = {\n type: `AccountActivityService:subscriptionError`;\n payload: [{ addresses: string[]; error: string; operation: string }];\n};\n\nexport type AccountActivityServiceStatusChangedEvent = {\n type: `AccountActivityService:statusChanged`;\n payload: [\n {\n chainIds: string[];\n status: 'up' | 'down';\n timestamp?: number;\n },\n ];\n};\n\nexport type AccountActivityServiceEvents =\n | AccountActivityServiceTransactionUpdatedEvent\n | AccountActivityServiceBalanceUpdatedEvent\n | AccountActivityServiceSubscriptionErrorEvent\n | AccountActivityServiceStatusChangedEvent;\n\nexport type AccountActivityServiceAllowedEvents =\n | AccountsControllerSelectedAccountChangeEvent\n | BackendWebSocketServiceConnectionStateChangedEvent;\n\nexport type AccountActivityServiceMessenger = RestrictedMessenger<\n typeof SERVICE_NAME,\n AccountActivityServiceActions | AccountActivityServiceAllowedActions,\n AccountActivityServiceEvents | AccountActivityServiceAllowedEvents,\n AccountActivityServiceAllowedActions['type'],\n AccountActivityServiceAllowedEvents['type']\n>;\n\n// =============================================================================\n// Main Service Class\n// =============================================================================\n\n/**\n * High-performance service for real-time account activity monitoring using optimized\n * WebSocket subscriptions with direct callback routing. Automatically subscribes to\n * the currently selected account and switches subscriptions when the selected account changes.\n * Receives transactions and balance updates using the comprehensive AccountActivityMessage format.\n *\n * Performance Features:\n * - Direct callback routing (no EventEmitter overhead)\n * - Minimal subscription tracking (no duplication with BackendWebSocketService)\n * - Optimized cleanup for mobile environments\n * - Single-account subscription (only selected account)\n * - Comprehensive balance updates with transfer tracking\n *\n * Architecture:\n * - Uses messenger pattern to communicate with BackendWebSocketService\n * - AccountActivityService tracks channel-to-subscriptionId mappings via messenger calls\n * - Automatically subscribes to selected account on initialization\n * - Switches subscriptions when selected account changes\n * - No direct dependency on BackendWebSocketService (uses messenger instead)\n *\n * @example\n * ```typescript\n * const service = new AccountActivityService({\n * messenger: activityMessenger,\n * });\n *\n * // Service automatically subscribes to the currently selected account\n * // When user switches accounts, service automatically resubscribes\n *\n * // All transactions and balance updates are received via optimized\n * // WebSocket callbacks and processed with zero-allocation routing\n * // Balance updates include comprehensive transfer details and post-transaction balances\n * ```\n */\nexport class AccountActivityService {\n /**\n * The name of the service.\n */\n readonly name = SERVICE_NAME;\n\n readonly #messenger: AccountActivityServiceMessenger;\n\n readonly #options: Required<AccountActivityServiceOptions>;\n\n // Track chains that are currently up (based on system notifications)\n readonly #chainsUp: Set<string> = new Set();\n\n // =============================================================================\n // Constructor and Initialization\n // =============================================================================\n\n /**\n * Creates a new Account Activity service instance\n *\n * @param options - Configuration options including messenger\n */\n constructor(\n options: AccountActivityServiceOptions & {\n messenger: AccountActivityServiceMessenger;\n },\n ) {\n this.#messenger = options.messenger;\n\n // Set configuration with defaults\n this.#options = {\n subscriptionNamespace:\n options.subscriptionNamespace ?? SUBSCRIPTION_NAMESPACE,\n };\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n this.#messenger.subscribe(\n 'AccountsController:selectedAccountChange',\n async (account: InternalAccount) =>\n await this.#handleSelectedAccountChange(account),\n );\n this.#messenger.subscribe(\n 'BackendWebSocketService:connectionStateChanged',\n (connectionInfo: WebSocketConnectionInfo) =>\n this.#handleWebSocketStateChange(connectionInfo),\n );\n this.#messenger.call('BackendWebSocketService:addChannelCallback', {\n channelName: `system-notifications.v1.${this.#options.subscriptionNamespace}`,\n callback: (notification: ServerNotificationMessage) =>\n this.#handleSystemNotification(notification),\n });\n }\n\n // =============================================================================\n // Account Subscription Methods\n // =============================================================================\n\n /**\n * Subscribe to account activity (transactions and balance updates)\n * Address should be in CAIP-10 format (e.g., \"eip155:0:0x1234...\" or \"solana:0:ABC123...\")\n *\n * @param subscription - Account subscription configuration with address\n */\n async subscribe(subscription: SubscriptionOptions): Promise<void> {\n try {\n await this.#messenger.call('BackendWebSocketService:connect');\n\n // Create channel name from address\n const channel = `${this.#options.subscriptionNamespace}.${subscription.address}`;\n\n // Check if already subscribed\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n return;\n }\n\n // Create subscription using the proper subscribe method (this will be stored in WebSocketService's internal tracking)\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n channelType: this.#options.subscriptionNamespace, // e.g., 'account-activity.v1'\n callback: (notification: ServerNotificationMessage) => {\n this.#handleAccountActivityUpdate(\n notification.data as AccountActivityMessage,\n );\n },\n });\n } catch (error) {\n log('Subscription failed, forcing reconnection', { error });\n await this.#forceReconnection();\n }\n }\n\n /**\n * Unsubscribe from account activity for specified address\n * Address should be in CAIP-10 format (e.g., \"eip155:0:0x1234...\" or \"solana:0:ABC123...\")\n *\n * @param subscription - Account subscription configuration with address to unsubscribe\n */\n async unsubscribe(subscription: SubscriptionOptions): Promise<void> {\n const { address } = subscription;\n try {\n // Find channel for the specified address\n const channel = `${this.#options.subscriptionNamespace}.${address}`;\n const subscriptions = this.#messenger.call(\n 'BackendWebSocketService:getSubscriptionsByChannel',\n channel,\n );\n\n if (subscriptions.length === 0) {\n return;\n }\n\n // Fast path: Direct unsubscribe using stored unsubscribe function\n // Unsubscribe from all matching subscriptions\n for (const subscriptionInfo of subscriptions) {\n await subscriptionInfo.unsubscribe();\n }\n } catch (error) {\n log('Unsubscription failed, forcing reconnection', { error });\n await this.#forceReconnection();\n }\n }\n\n // =============================================================================\n // Private Methods - Event Handlers\n // =============================================================================\n\n /**\n * Handle account activity updates (transactions + balance changes)\n * Processes the comprehensive AccountActivityMessage format with detailed balance updates and transfers\n *\n * @param payload - The account activity message containing transaction and balance updates\n * @example AccountActivityMessage format handling:\n * Input: {\n * address: \"0xd14b52362b5b777ffa754c666ddec6722aaeee08\",\n * tx: { id: \"0x1cde...\", chain: \"eip155:8453\", status: \"confirmed\", timestamp: 1760099871, ... },\n * updates: [{\n * asset: { fungible: true, type: \"eip155:8453/erc20:0x833...\", unit: \"USDC\", decimals: 6 },\n * postBalance: { amount: \"0xc350\" },\n * transfers: [{ from: \"0x7b07...\", to: \"0xd14b...\", amount: \"0x2710\" }]\n * }]\n * }\n * Output: Transaction and balance updates published separately\n */\n #handleAccountActivityUpdate(payload: AccountActivityMessage): void {\n const { address, tx, updates } = payload;\n\n log('Handling account activity update', {\n address,\n updateCount: updates.length,\n });\n\n // Process transaction update\n this.#messenger.publish(`AccountActivityService:transactionUpdated`, tx);\n\n // Publish comprehensive balance updates with transfer details\n this.#messenger.publish(`AccountActivityService:balanceUpdated`, {\n address,\n chain: tx.chain,\n updates,\n });\n }\n\n /**\n * Handle selected account change event\n *\n * @param newAccount - The newly selected account\n */\n async #handleSelectedAccountChange(\n newAccount: InternalAccount | null,\n ): Promise<void> {\n if (!newAccount?.address) {\n return;\n }\n\n try {\n // Convert new account to CAIP-10 format\n const newAddress = this.#convertToCaip10Address(newAccount);\n\n // First, unsubscribe from all current account activity subscriptions to avoid multiple subscriptions\n await this.#unsubscribeFromAllAccountActivity();\n\n // Then, subscribe to the new selected account\n await this.subscribe({ address: newAddress });\n } catch (error) {\n log('Account change failed', { error });\n }\n }\n\n /**\n * Handle system notification for chain status changes\n * Publishes only the status change (delta) for affected chains\n *\n * @param notification - Server notification message containing chain status updates and timestamp\n */\n #handleSystemNotification(notification: ServerNotificationMessage): void {\n const data = notification.data as SystemNotificationData;\n const { timestamp } = notification;\n\n // Validate required fields\n if (!data.chainIds || !Array.isArray(data.chainIds) || !data.status) {\n throw new Error(\n 'Invalid system notification data: missing chainIds or status',\n );\n }\n\n // Track chain status\n if (data.status === 'up') {\n for (const chainId of data.chainIds) {\n this.#chainsUp.add(chainId);\n }\n } else {\n for (const chainId of data.chainIds) {\n this.#chainsUp.delete(chainId);\n }\n }\n\n // Publish status change directly (delta update)\n this.#messenger.publish(`AccountActivityService:statusChanged`, {\n chainIds: data.chainIds,\n status: data.status,\n timestamp,\n });\n }\n\n /**\n * Handle WebSocket connection state changes for fallback polling and resubscription\n *\n * @param connectionInfo - WebSocket connection state information\n */\n async #handleWebSocketStateChange(\n connectionInfo: WebSocketConnectionInfo,\n ): Promise<void> {\n const { state } = connectionInfo;\n\n if (state === WebSocketState.CONNECTED) {\n // WebSocket connected - resubscribe to selected account\n // The system notification will automatically provide the list of chains that are up\n await this.#subscribeToSelectedAccount();\n } else if (\n state === WebSocketState.DISCONNECTED ||\n state === WebSocketState.ERROR\n ) {\n // On disconnect/error, flush all tracked chains as down\n const chainsToMarkDown = Array.from(this.#chainsUp);\n\n if (chainsToMarkDown.length > 0) {\n this.#messenger.publish(`AccountActivityService:statusChanged`, {\n chainIds: chainsToMarkDown,\n status: 'down',\n });\n\n log(\n 'WebSocket error/disconnection - Published tracked chains as down',\n {\n count: chainsToMarkDown.length,\n chains: chainsToMarkDown,\n },\n );\n\n // Clear the tracking set since all chains are now down\n this.#chainsUp.clear();\n }\n }\n }\n\n // =============================================================================\n // Private Methods - Subscription Management\n // =============================================================================\n\n /**\n * Subscribe to the currently selected account only\n */\n async #subscribeToSelectedAccount(): Promise<void> {\n const selectedAccount = this.#messenger.call(\n 'AccountsController:getSelectedAccount',\n );\n\n if (!selectedAccount || !selectedAccount.address) {\n return;\n }\n\n // Convert to CAIP-10 format and subscribe\n const address = this.#convertToCaip10Address(selectedAccount);\n await this.subscribe({ address });\n }\n\n /**\n * Unsubscribe from all account activity subscriptions for this service\n * Finds all channels matching the service's namespace and unsubscribes from them\n */\n async #unsubscribeFromAllAccountActivity(): Promise<void> {\n const accountActivitySubscriptions = this.#messenger.call(\n 'BackendWebSocketService:findSubscriptionsByChannelPrefix',\n this.#options.subscriptionNamespace,\n );\n\n // Unsubscribe from all matching subscriptions\n for (const subscription of accountActivitySubscriptions) {\n await subscription.unsubscribe();\n }\n }\n\n // =============================================================================\n // Private Methods - Utility Functions\n // =============================================================================\n\n /**\n * Convert an InternalAccount address to CAIP-10 format or raw address\n *\n * @param account - The internal account to convert\n * @returns The CAIP-10 formatted address or raw address\n */\n #convertToCaip10Address(account: InternalAccount): string {\n // Check if account has EVM scopes\n if (account.scopes.some((scope) => scope.startsWith('eip155:'))) {\n // CAIP-10 format: eip155:0:address (subscribe to all EVM chains)\n return `eip155:0:${account.address}`;\n }\n\n // Check if account has Solana scopes\n if (account.scopes.some((scope) => scope.startsWith('solana:'))) {\n // CAIP-10 format: solana:0:address (subscribe to all Solana chains)\n return `solana:0:${account.address}`;\n }\n\n // For other chains or unknown scopes, return raw address\n return account.address;\n }\n\n /**\n * Force WebSocket reconnection to clean up subscription state\n */\n async #forceReconnection(): Promise<void> {\n try {\n log('Forcing WebSocket reconnection to clean up subscription state');\n\n // All subscriptions will be cleaned up automatically on WebSocket disconnect\n\n await this.#messenger.call('BackendWebSocketService:disconnect');\n await this.#messenger.call('BackendWebSocketService:connect');\n } catch (error) {\n log('Failed to force WebSocket reconnection', { error });\n }\n }\n\n // =============================================================================\n // Public Methods - Cleanup\n // =============================================================================\n\n /**\n * Destroy the service and clean up all resources\n * Optimized for fast cleanup during service destruction or mobile app termination\n */\n destroy(): void {\n // Clean up system notification callback\n this.#messenger.call(\n 'BackendWebSocketService:removeChannelCallback',\n `system-notifications.v1.${this.#options.subscriptionNamespace}`,\n );\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"AccountActivityService.mjs","sourceRoot":"","sources":["../src/AccountActivityService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Account Activity Service for monitoring account transactions and balance changes\n *\n * This service subscribes to account activity and receives all transactions\n * and balance updates for those accounts via the comprehensive AccountActivityMessage format.\n */\n\nimport type {\n AccountsControllerGetSelectedAccountAction,\n AccountsControllerSelectedAccountChangeEvent,\n} from '@metamask/accounts-controller';\nimport type { RestrictedMessenger } from '@metamask/base-controller';\nimport type { InternalAccount } from '@metamask/keyring-internal-api';\n\nimport type { AccountActivityServiceMethodActions } from './AccountActivityService-method-action-types';\nimport type {\n WebSocketConnectionInfo,\n BackendWebSocketServiceConnectionStateChangedEvent,\n ServerNotificationMessage,\n} from './BackendWebSocketService';\nimport { WebSocketState } from './BackendWebSocketService';\nimport type { BackendWebSocketServiceMethodActions } from './BackendWebSocketService-method-action-types';\nimport { projectLogger, createModuleLogger } from './logger';\nimport type {\n Transaction,\n AccountActivityMessage,\n BalanceUpdate,\n} from './types';\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Fetches supported networks from the v2 API endpoint.\n * Returns chain IDs already in CAIP-2 format.\n *\n * Note: This directly calls the Account API v2 endpoint. In the future, this should\n * be moved to a dedicated data layer service for better separation of concerns.\n *\n * @returns Array of supported chain IDs in CAIP-2 format (e.g., \"eip155:1\")\n */\nasync function fetchSupportedChainsInCaipFormat(): Promise<string[]> {\n const url = 'https://accounts.api.cx.metamask.io/v2/supportedNetworks';\n const response = await fetch(url);\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch supported networks: ${response.status} ${response.statusText}`,\n );\n }\n\n const data: {\n fullSupport: string[];\n partialSupport: { balances: string[] };\n } = await response.json();\n\n // v2 endpoint already returns data in CAIP-2 format\n return data.fullSupport;\n}\n\n// =============================================================================\n// Types and Constants\n// =============================================================================\n\n/**\n * System notification data for chain status updates\n */\nexport type SystemNotificationData = {\n /** Array of chain IDs affected (e.g., ['eip155:137', 'eip155:1']) */\n chainIds: string[];\n /** Status of the chains: 'down' or 'up' */\n status: 'down' | 'up';\n};\n\nconst SERVICE_NAME = 'AccountActivityService';\n\nconst log = createModuleLogger(projectLogger, SERVICE_NAME);\n\nconst MESSENGER_EXPOSED_METHODS = ['subscribe', 'unsubscribe'] as const;\n\n// Default supported chains used as fallback when API is unavailable\n// This list should match the expected chains from the accounts API v2/supportedNetworks endpoint\nconst DEFAULT_SUPPORTED_CHAINS = [\n 'eip155:1', // Ethereum Mainnet\n 'eip155:137', // Polygon\n 'eip155:56', // BSC\n 'eip155:59144', // Linea\n 'eip155:8453', // Base\n 'eip155:10', // Optimism\n 'eip155:42161', // Arbitrum One\n 'eip155:534352', // Scroll\n 'eip155:1329', // Sei\n];\nconst SUBSCRIPTION_NAMESPACE = 'account-activity.v1';\n\n// Cache TTL for supported chains (5 hours in milliseconds)\nconst SUPPORTED_CHAINS_CACHE_TTL = 5 * 60 * 60 * 1000;\n\n/**\n * Account subscription options\n */\nexport type SubscriptionOptions = {\n address: string; // Should be in CAIP-10 format, e.g., \"eip155:0:0x1234...\" or \"solana:0:ABC123...\"\n};\n\n/**\n * Configuration options for the account activity service\n */\nexport type AccountActivityServiceOptions = {\n /** Custom subscription namespace (default: 'account-activity.v1') */\n subscriptionNamespace?: string;\n};\n\n// =============================================================================\n// Action and Event Types\n// =============================================================================\n\n// Action types for the messaging system - using generated method actions\nexport type AccountActivityServiceActions = AccountActivityServiceMethodActions;\n\n// Allowed actions that AccountActivityService can call on other controllers\nexport const ACCOUNT_ACTIVITY_SERVICE_ALLOWED_ACTIONS = [\n 'AccountsController:getSelectedAccount',\n 'BackendWebSocketService:connect',\n 'BackendWebSocketService:disconnect',\n 'BackendWebSocketService:subscribe',\n 'BackendWebSocketService:getConnectionInfo',\n 'BackendWebSocketService:channelHasSubscription',\n 'BackendWebSocketService:getSubscriptionsByChannel',\n 'BackendWebSocketService:findSubscriptionsByChannelPrefix',\n 'BackendWebSocketService:addChannelCallback',\n 'BackendWebSocketService:removeChannelCallback',\n] as const;\n\n// Allowed events that AccountActivityService can listen to\nexport const ACCOUNT_ACTIVITY_SERVICE_ALLOWED_EVENTS = [\n 'AccountsController:selectedAccountChange',\n 'BackendWebSocketService:connectionStateChanged',\n] as const;\n\nexport type AccountActivityServiceAllowedActions =\n | AccountsControllerGetSelectedAccountAction\n | BackendWebSocketServiceMethodActions;\n\n// Event types for the messaging system\n\nexport type AccountActivityServiceTransactionUpdatedEvent = {\n type: `AccountActivityService:transactionUpdated`;\n payload: [Transaction];\n};\n\nexport type AccountActivityServiceBalanceUpdatedEvent = {\n type: `AccountActivityService:balanceUpdated`;\n payload: [{ address: string; chain: string; updates: BalanceUpdate[] }];\n};\n\nexport type AccountActivityServiceSubscriptionErrorEvent = {\n type: `AccountActivityService:subscriptionError`;\n payload: [{ addresses: string[]; error: string; operation: string }];\n};\n\nexport type AccountActivityServiceStatusChangedEvent = {\n type: `AccountActivityService:statusChanged`;\n payload: [\n {\n chainIds: string[];\n status: 'up' | 'down';\n },\n ];\n};\n\nexport type AccountActivityServiceEvents =\n | AccountActivityServiceTransactionUpdatedEvent\n | AccountActivityServiceBalanceUpdatedEvent\n | AccountActivityServiceSubscriptionErrorEvent\n | AccountActivityServiceStatusChangedEvent;\n\nexport type AccountActivityServiceAllowedEvents =\n | AccountsControllerSelectedAccountChangeEvent\n | BackendWebSocketServiceConnectionStateChangedEvent;\n\nexport type AccountActivityServiceMessenger = RestrictedMessenger<\n typeof SERVICE_NAME,\n AccountActivityServiceActions | AccountActivityServiceAllowedActions,\n AccountActivityServiceEvents | AccountActivityServiceAllowedEvents,\n AccountActivityServiceAllowedActions['type'],\n AccountActivityServiceAllowedEvents['type']\n>;\n\n// =============================================================================\n// Main Service Class\n// =============================================================================\n\n/**\n * High-performance service for real-time account activity monitoring using optimized\n * WebSocket subscriptions with direct callback routing. Automatically subscribes to\n * the currently selected account and switches subscriptions when the selected account changes.\n * Receives transactions and balance updates using the comprehensive AccountActivityMessage format.\n *\n * Performance Features:\n * - Direct callback routing (no EventEmitter overhead)\n * - Minimal subscription tracking (no duplication with BackendWebSocketService)\n * - Optimized cleanup for mobile environments\n * - Single-account subscription (only selected account)\n * - Comprehensive balance updates with transfer tracking\n *\n * Architecture:\n * - Uses messenger pattern to communicate with BackendWebSocketService\n * - AccountActivityService tracks channel-to-subscriptionId mappings via messenger calls\n * - Automatically subscribes to selected account on initialization\n * - Switches subscriptions when selected account changes\n * - No direct dependency on BackendWebSocketService (uses messenger instead)\n *\n * @example\n * ```typescript\n * const service = new AccountActivityService({\n * messenger: activityMessenger,\n * });\n *\n * // Service automatically subscribes to the currently selected account\n * // When user switches accounts, service automatically resubscribes\n *\n * // All transactions and balance updates are received via optimized\n * // WebSocket callbacks and processed with zero-allocation routing\n * // Balance updates include comprehensive transfer details and post-transaction balances\n * ```\n */\nexport class AccountActivityService {\n /**\n * The name of the service.\n */\n readonly name = SERVICE_NAME;\n\n readonly #messenger: AccountActivityServiceMessenger;\n\n readonly #options: Required<AccountActivityServiceOptions>;\n\n #supportedChains: string[] | null = null;\n\n #supportedChainsExpiresAt: number = 0;\n\n // =============================================================================\n // Constructor and Initialization\n // =============================================================================\n\n /**\n * Creates a new Account Activity service instance\n *\n * @param options - Configuration options including messenger\n */\n constructor(\n options: AccountActivityServiceOptions & {\n messenger: AccountActivityServiceMessenger;\n },\n ) {\n this.#messenger = options.messenger;\n\n // Set configuration with defaults\n this.#options = {\n subscriptionNamespace:\n options.subscriptionNamespace ?? SUBSCRIPTION_NAMESPACE,\n };\n\n this.#messenger.registerMethodActionHandlers(\n this,\n MESSENGER_EXPOSED_METHODS,\n );\n this.#messenger.subscribe(\n 'AccountsController:selectedAccountChange',\n async (account: InternalAccount) =>\n await this.#handleSelectedAccountChange(account),\n );\n this.#messenger.subscribe(\n 'BackendWebSocketService:connectionStateChanged',\n (connectionInfo: WebSocketConnectionInfo) =>\n this.#handleWebSocketStateChange(connectionInfo),\n );\n this.#messenger.call('BackendWebSocketService:addChannelCallback', {\n channelName: `system-notifications.v1.${this.#options.subscriptionNamespace}`,\n callback: (notification: ServerNotificationMessage) =>\n this.#handleSystemNotification(\n notification.data as SystemNotificationData,\n ),\n });\n }\n\n // =============================================================================\n // Public Methods - Chain Management\n // =============================================================================\n\n /**\n * Fetch supported chains from API with fallback to hardcoded list.\n * Uses expiry-based caching with TTL to prevent stale data.\n *\n * @returns Array of supported chain IDs in CAIP-2 format\n */\n async getSupportedChains(): Promise<string[]> {\n // Return cached result if available and not expired\n if (\n this.#supportedChains !== null &&\n Date.now() < this.#supportedChainsExpiresAt\n ) {\n return this.#supportedChains;\n }\n\n try {\n // Try to fetch from API\n this.#supportedChains = await fetchSupportedChainsInCaipFormat();\n } catch {\n // Fallback to hardcoded list and cache it with timestamp\n this.#supportedChains = Array.from(DEFAULT_SUPPORTED_CHAINS);\n }\n\n this.#supportedChainsExpiresAt = Date.now() + SUPPORTED_CHAINS_CACHE_TTL;\n\n return this.#supportedChains;\n }\n\n // =============================================================================\n // Account Subscription Methods\n // =============================================================================\n\n /**\n * Subscribe to account activity (transactions and balance updates)\n * Address should be in CAIP-10 format (e.g., \"eip155:0:0x1234...\" or \"solana:0:ABC123...\")\n *\n * @param subscription - Account subscription configuration with address\n */\n async subscribe(subscription: SubscriptionOptions): Promise<void> {\n try {\n await this.#messenger.call('BackendWebSocketService:connect');\n\n // Create channel name from address\n const channel = `${this.#options.subscriptionNamespace}.${subscription.address}`;\n\n // Check if already subscribed\n if (\n this.#messenger.call(\n 'BackendWebSocketService:channelHasSubscription',\n channel,\n )\n ) {\n return;\n }\n\n // Create subscription using the proper subscribe method (this will be stored in WebSocketService's internal tracking)\n await this.#messenger.call('BackendWebSocketService:subscribe', {\n channels: [channel],\n callback: (notification: ServerNotificationMessage) => {\n this.#handleAccountActivityUpdate(\n notification.data as AccountActivityMessage,\n );\n },\n });\n } catch (error) {\n log('Subscription failed, forcing reconnection', { error });\n await this.#forceReconnection();\n }\n }\n\n /**\n * Unsubscribe from account activity for specified address\n * Address should be in CAIP-10 format (e.g., \"eip155:0:0x1234...\" or \"solana:0:ABC123...\")\n *\n * @param subscription - Account subscription configuration with address to unsubscribe\n */\n async unsubscribe(subscription: SubscriptionOptions): Promise<void> {\n const { address } = subscription;\n try {\n // Find channel for the specified address\n const channel = `${this.#options.subscriptionNamespace}.${address}`;\n const subscriptions = this.#messenger.call(\n 'BackendWebSocketService:getSubscriptionsByChannel',\n channel,\n );\n\n if (subscriptions.length === 0) {\n return;\n }\n\n // Fast path: Direct unsubscribe using stored unsubscribe function\n // Unsubscribe from all matching subscriptions\n for (const subscriptionInfo of subscriptions) {\n await subscriptionInfo.unsubscribe();\n }\n } catch (error) {\n log('Unsubscription failed, forcing reconnection', { error });\n await this.#forceReconnection();\n }\n }\n\n // =============================================================================\n // Private Methods - Event Handlers\n // =============================================================================\n\n /**\n * Handle account activity updates (transactions + balance changes)\n * Processes the comprehensive AccountActivityMessage format with detailed balance updates and transfers\n *\n * @param payload - The account activity message containing transaction and balance updates\n * @example AccountActivityMessage format handling:\n * Input: {\n * address: \"0x123\",\n * tx: { hash: \"0x...\", chain: \"eip155:1\", status: \"completed\", ... },\n * updates: [{\n * asset: { fungible: true, type: \"eip155:1/erc20:0x...\", unit: \"USDT\" },\n * postBalance: { amount: \"1254.75\" },\n * transfers: [{ from: \"0x...\", to: \"0x...\", amount: \"500.00\" }]\n * }]\n * }\n * Output: Transaction and balance updates published separately\n */\n #handleAccountActivityUpdate(payload: AccountActivityMessage): void {\n const { address, tx, updates } = payload;\n\n log('Handling account activity update', {\n address,\n updateCount: updates.length,\n });\n\n // Process transaction update\n this.#messenger.publish(`AccountActivityService:transactionUpdated`, tx);\n\n // Publish comprehensive balance updates with transfer details\n this.#messenger.publish(`AccountActivityService:balanceUpdated`, {\n address,\n chain: tx.chain,\n updates,\n });\n }\n\n /**\n * Handle selected account change event\n *\n * @param newAccount - The newly selected account\n */\n async #handleSelectedAccountChange(\n newAccount: InternalAccount | null,\n ): Promise<void> {\n if (!newAccount?.address) {\n return;\n }\n\n try {\n // Convert new account to CAIP-10 format\n const newAddress = this.#convertToCaip10Address(newAccount);\n\n // First, unsubscribe from all current account activity subscriptions to avoid multiple subscriptions\n await this.#unsubscribeFromAllAccountActivity();\n\n // Then, subscribe to the new selected account\n await this.subscribe({ address: newAddress });\n } catch (error) {\n log('Account change failed', { error });\n }\n }\n\n /**\n * Handle system notification for chain status changes\n * Publishes only the status change (delta) for affected chains\n *\n * @param data - System notification data containing chain status updates\n */\n #handleSystemNotification(data: SystemNotificationData): void {\n // Validate required fields\n if (!data.chainIds || !Array.isArray(data.chainIds) || !data.status) {\n throw new Error(\n 'Invalid system notification data: missing chainIds or status',\n );\n }\n\n // Publish status change directly (delta update)\n this.#messenger.publish(`AccountActivityService:statusChanged`, {\n chainIds: data.chainIds,\n status: data.status,\n });\n }\n\n /**\n * Handle WebSocket connection state changes for fallback polling and resubscription\n *\n * @param connectionInfo - WebSocket connection state information\n */\n async #handleWebSocketStateChange(\n connectionInfo: WebSocketConnectionInfo,\n ): Promise<void> {\n const { state } = connectionInfo;\n const supportedChains = await this.getSupportedChains();\n\n if (state === WebSocketState.CONNECTED) {\n // WebSocket connected - resubscribe and set all chains as up\n await this.#subscribeToSelectedAccount();\n\n // Publish initial status - all supported chains are up when WebSocket connects\n this.#messenger.publish(`AccountActivityService:statusChanged`, {\n chainIds: supportedChains,\n status: 'up',\n });\n\n log('WebSocket connected - Published all chains as up', {\n count: supportedChains.length,\n chains: supportedChains,\n });\n } else if (\n state === WebSocketState.DISCONNECTED ||\n state === WebSocketState.ERROR\n ) {\n this.#messenger.publish(`AccountActivityService:statusChanged`, {\n chainIds: supportedChains,\n status: 'down',\n });\n\n log('WebSocket error/disconnection - Published all chains as down', {\n count: supportedChains.length,\n chains: supportedChains,\n });\n }\n }\n\n // =============================================================================\n // Private Methods - Subscription Management\n // =============================================================================\n\n /**\n * Subscribe to the currently selected account only\n */\n async #subscribeToSelectedAccount(): Promise<void> {\n const selectedAccount = this.#messenger.call(\n 'AccountsController:getSelectedAccount',\n );\n\n if (!selectedAccount || !selectedAccount.address) {\n return;\n }\n\n // Convert to CAIP-10 format and subscribe\n const address = this.#convertToCaip10Address(selectedAccount);\n await this.subscribe({ address });\n }\n\n /**\n * Unsubscribe from all account activity subscriptions for this service\n * Finds all channels matching the service's namespace and unsubscribes from them\n */\n async #unsubscribeFromAllAccountActivity(): Promise<void> {\n const accountActivitySubscriptions = this.#messenger.call(\n 'BackendWebSocketService:findSubscriptionsByChannelPrefix',\n this.#options.subscriptionNamespace,\n );\n\n // Unsubscribe from all matching subscriptions\n for (const subscription of accountActivitySubscriptions) {\n await subscription.unsubscribe();\n }\n }\n\n // =============================================================================\n // Private Methods - Utility Functions\n // =============================================================================\n\n /**\n * Convert an InternalAccount address to CAIP-10 format or raw address\n *\n * @param account - The internal account to convert\n * @returns The CAIP-10 formatted address or raw address\n */\n #convertToCaip10Address(account: InternalAccount): string {\n // Check if account has EVM scopes\n if (account.scopes.some((scope) => scope.startsWith('eip155:'))) {\n // CAIP-10 format: eip155:0:address (subscribe to all EVM chains)\n return `eip155:0:${account.address}`;\n }\n\n // Check if account has Solana scopes\n if (account.scopes.some((scope) => scope.startsWith('solana:'))) {\n // CAIP-10 format: solana:0:address (subscribe to all Solana chains)\n return `solana:0:${account.address}`;\n }\n\n // For other chains or unknown scopes, return raw address\n return account.address;\n }\n\n /**\n * Force WebSocket reconnection to clean up subscription state\n */\n async #forceReconnection(): Promise<void> {\n try {\n log('Forcing WebSocket reconnection to clean up subscription state');\n\n // All subscriptions will be cleaned up automatically on WebSocket disconnect\n\n await this.#messenger.call('BackendWebSocketService:disconnect');\n await this.#messenger.call('BackendWebSocketService:connect');\n } catch (error) {\n log('Failed to force WebSocket reconnection', { error });\n }\n }\n\n // =============================================================================\n // Public Methods - Cleanup\n // =============================================================================\n\n /**\n * Destroy the service and clean up all resources\n * Optimized for fast cleanup during service destruction or mobile app termination\n */\n destroy(): void {\n // Clean up system notification callback\n this.#messenger.call(\n 'BackendWebSocketService:removeChannelCallback',\n `system-notifications.v1.${this.#options.subscriptionNamespace}`,\n );\n }\n}\n"]}
|