@metamask-previews/assets-controller 0.0.0-preview-2aeb1204 → 0.0.0-preview-40468f94

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 (33) hide show
  1. package/dist/data-sources/SnapDataSource.cjs +464 -193
  2. package/dist/data-sources/SnapDataSource.cjs.map +1 -1
  3. package/dist/data-sources/SnapDataSource.d.cts +86 -24
  4. package/dist/data-sources/SnapDataSource.d.cts.map +1 -1
  5. package/dist/data-sources/SnapDataSource.d.mts +86 -24
  6. package/dist/data-sources/SnapDataSource.d.mts.map +1 -1
  7. package/dist/data-sources/SnapDataSource.mjs +458 -191
  8. package/dist/data-sources/SnapDataSource.mjs.map +1 -1
  9. package/dist/data-sources/index.cjs +36 -7
  10. package/dist/data-sources/index.cjs.map +1 -1
  11. package/dist/data-sources/index.d.cts +1 -1
  12. package/dist/data-sources/index.d.cts.map +1 -1
  13. package/dist/data-sources/index.d.mts +1 -1
  14. package/dist/data-sources/index.d.mts.map +1 -1
  15. package/dist/data-sources/index.mjs +13 -5
  16. package/dist/data-sources/index.mjs.map +1 -1
  17. package/dist/data-sources/initDataSources.cjs +3 -9
  18. package/dist/data-sources/initDataSources.cjs.map +1 -1
  19. package/dist/data-sources/initDataSources.d.cts +4 -2
  20. package/dist/data-sources/initDataSources.d.cts.map +1 -1
  21. package/dist/data-sources/initDataSources.d.mts +4 -2
  22. package/dist/data-sources/initDataSources.d.mts.map +1 -1
  23. package/dist/data-sources/initDataSources.mjs +3 -9
  24. package/dist/data-sources/initDataSources.mjs.map +1 -1
  25. package/dist/index.cjs +36 -7
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +2 -2
  28. package/dist/index.d.cts.map +1 -1
  29. package/dist/index.d.mts +2 -2
  30. package/dist/index.d.mts.map +1 -1
  31. package/dist/index.mjs +13 -5
  32. package/dist/index.mjs.map +1 -1
  33. package/package.json +1 -6
@@ -9,9 +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 _SnapDataSource_instances, _SnapDataSource_messenger, _SnapDataSource_handleSnapBalancesUpdatedBound, _SnapDataSource_handlePermissionStateChangeBound, _SnapDataSource_keyringClientCache, _SnapDataSource_subscribeToEvents, _SnapDataSource_handleSnapBalancesUpdated, _SnapDataSource_isChainSupportedBySnap, _SnapDataSource_registerActionHandlers, _SnapDataSource_getRunnableSnaps, _SnapDataSource_getSnapPermissions, _SnapDataSource_discoverKeyringSnaps, _SnapDataSource_getKeyringClient;
13
- import { KeyringClient } from "@metamask/keyring-snap-client";
14
- import { HandlerType, SnapCaveatType } from "@metamask/snaps-utils";
12
+ var _SnapDataSource_instances, _SnapDataSource_messenger, _SnapDataSource_snapProvider, _SnapDataSource_subscribeToSnapKeyringEvents, _SnapDataSource_handleSnapBalancesUpdated, _SnapDataSource_registerActionHandlers, _SnapDataSource_getInstalledSnaps, _SnapDataSource_checkSnapAvailabilityOnDemand, _SnapDataSource_checkAllSnapsAvailability, _SnapDataSource_accountSupportsChain, _SnapDataSource_groupChainsBySnap, _SnapDataSource_fetchFromSnap;
15
13
  import { AbstractDataSource } from "./AbstractDataSource.mjs";
16
14
  import { projectLogger, createModuleLogger } from "../logger.mjs";
17
15
  const log = createModuleLogger(projectLogger, 'SnapDataSource');
@@ -19,29 +17,89 @@ const log = createModuleLogger(projectLogger, 'SnapDataSource');
19
17
  // CONSTANTS
20
18
  // ============================================================================
21
19
  export const SNAP_DATA_SOURCE_NAME = 'SnapDataSource';
22
- /** The permission name for snap keyring endowment */
23
- export const KEYRING_PERMISSION = 'endowment:keyring';
24
- /** The permission name for snap assets endowment (contains chainIds) */
25
- export const ASSETS_PERMISSION = 'endowment:assets';
26
- // ============================================================================
27
- // PERMISSION UTILITIES
28
- // ============================================================================
20
+ // Snap IDs
21
+ export const SOLANA_SNAP_ID = 'npm:@metamask/solana-wallet-snap';
22
+ export const BITCOIN_SNAP_ID = 'npm:@metamask/bitcoin-wallet-snap';
23
+ export const TRON_SNAP_ID = 'npm:@metamask/tron-wallet-snap';
24
+ // Chain prefixes for detection
25
+ export const SOLANA_CHAIN_PREFIX = 'solana:';
26
+ export const BITCOIN_CHAIN_PREFIX = 'bip122:';
27
+ export const TRON_CHAIN_PREFIX = 'tron:';
28
+ // Default networks
29
+ export const SOLANA_MAINNET = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp';
30
+ export const SOLANA_DEVNET = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
31
+ export const SOLANA_TESTNET = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z';
32
+ export const BITCOIN_MAINNET = 'bip122:000000000019d6689c085ae165831e93';
33
+ export const BITCOIN_TESTNET = 'bip122:000000000933ea01ad0ee984209779ba';
34
+ export const TRON_MAINNET = 'tron:728126428';
35
+ export const TRON_SHASTA = 'tron:2494104990';
36
+ export const TRON_NILE = 'tron:3448148188';
37
+ // Hex format alternatives for Tron
38
+ export const TRON_MAINNET_HEX = 'tron:0x2b6653dc';
39
+ export const TRON_SHASTA_HEX = 'tron:0x94a9059e';
40
+ export const TRON_NILE_HEX = 'tron:0xcd8690dc';
41
+ // Default poll intervals
42
+ export const DEFAULT_SOLANA_POLL_INTERVAL = 30000; // 30 seconds
43
+ export const DEFAULT_BITCOIN_POLL_INTERVAL = 60000; // 1 minute
44
+ export const DEFAULT_TRON_POLL_INTERVAL = 30000; // 30 seconds
45
+ export const DEFAULT_SNAP_POLL_INTERVAL = 30000; // Default for unknown snaps
46
+ // All default networks
47
+ export const ALL_DEFAULT_NETWORKS = [
48
+ SOLANA_MAINNET,
49
+ SOLANA_DEVNET,
50
+ SOLANA_TESTNET,
51
+ BITCOIN_MAINNET,
52
+ BITCOIN_TESTNET,
53
+ TRON_MAINNET,
54
+ TRON_SHASTA,
55
+ TRON_NILE,
56
+ TRON_MAINNET_HEX,
57
+ TRON_SHASTA_HEX,
58
+ TRON_NILE_HEX,
59
+ ];
60
+ export const SNAP_REGISTRY = {
61
+ solana: {
62
+ snapId: SOLANA_SNAP_ID,
63
+ chainPrefix: SOLANA_CHAIN_PREFIX,
64
+ pollInterval: DEFAULT_SOLANA_POLL_INTERVAL,
65
+ },
66
+ bitcoin: {
67
+ snapId: BITCOIN_SNAP_ID,
68
+ chainPrefix: BITCOIN_CHAIN_PREFIX,
69
+ pollInterval: DEFAULT_BITCOIN_POLL_INTERVAL,
70
+ },
71
+ tron: {
72
+ snapId: TRON_SNAP_ID,
73
+ chainPrefix: TRON_CHAIN_PREFIX,
74
+ pollInterval: DEFAULT_TRON_POLL_INTERVAL,
75
+ },
76
+ };
29
77
  /**
30
- * Getter function to get the chainIds caveat from a permission.
31
- *
32
- * This does basic validation of the caveat, but does not validate the type or
33
- * value of the namespaces object itself, as this is handled by the
34
- * `PermissionsController` when the permission is requested.
78
+ * Get the snap type for a chain ID based on its prefix.
35
79
  *
36
- * @param permission - The permission to get the `chainIds` caveat from.
37
- * @returns An array of `chainIds` that the snap supports, or null if none.
80
+ * @param chainId - The CAIP-2 chain ID to check.
81
+ * @returns The snap type for the chain, or null if not supported.
38
82
  */
39
- export function getChainIdsCaveat(permission) {
40
- if (!permission?.caveats) {
41
- return null;
83
+ export function getSnapTypeForChain(chainId) {
84
+ if (chainId.startsWith(SOLANA_CHAIN_PREFIX)) {
85
+ return 'solana';
42
86
  }
43
- const caveat = permission.caveats.find((permCaveat) => permCaveat.type === SnapCaveatType.ChainIds);
44
- return caveat ? caveat.value : null;
87
+ if (chainId.startsWith(BITCOIN_CHAIN_PREFIX)) {
88
+ return 'bitcoin';
89
+ }
90
+ if (chainId.startsWith(TRON_CHAIN_PREFIX)) {
91
+ return 'tron';
92
+ }
93
+ return null;
94
+ }
95
+ /**
96
+ * Check if a chain ID is supported by a snap.
97
+ *
98
+ * @param chainId - The CAIP-2 chain ID to check.
99
+ * @returns True if the chain is supported by a snap.
100
+ */
101
+ export function isSnapSupportedChain(chainId) {
102
+ return getSnapTypeForChain(chainId) !== null;
45
103
  }
46
104
  /**
47
105
  * Extract chain ID from a CAIP-19 asset ID.
@@ -54,9 +112,23 @@ export function extractChainFromAssetId(assetId) {
54
112
  const parts = assetId.split('/');
55
113
  return parts[0];
56
114
  }
115
+ // Helper functions for specific chain types
116
+ export function isSolanaChain(chainId) {
117
+ return chainId.startsWith(SOLANA_CHAIN_PREFIX);
118
+ }
119
+ export function isBitcoinChain(chainId) {
120
+ return chainId.startsWith(BITCOIN_CHAIN_PREFIX);
121
+ }
122
+ export function isTronChain(chainId) {
123
+ return chainId.startsWith(TRON_CHAIN_PREFIX);
124
+ }
57
125
  const defaultSnapState = {
58
- activeChains: [],
59
- chainToSnap: {},
126
+ activeChains: ALL_DEFAULT_NETWORKS,
127
+ snaps: {
128
+ solana: { version: null, available: false },
129
+ bitcoin: { version: null, available: false },
130
+ tron: { version: null, available: false },
131
+ },
60
132
  };
61
133
  // ============================================================================
62
134
  // SNAP DATA SOURCE
@@ -65,10 +137,16 @@ const defaultSnapState = {
65
137
  * Unified Snap data source that routes requests to the appropriate wallet snap
66
138
  * based on the chain ID prefix.
67
139
  *
140
+ * Supports:
141
+ * - Solana chains (solana:*) → @metamask/solana-wallet-snap
142
+ * - Bitcoin chains (bip122:*) → @metamask/bitcoin-wallet-snap
143
+ * - Tron chains (tron:*) → @metamask/tron-wallet-snap
144
+ *
68
145
  * @example
69
146
  * ```typescript
70
147
  * const snapDataSource = new SnapDataSource({
71
148
  * messenger,
149
+ * snapProvider: metamaskProvider,
72
150
  * });
73
151
  *
74
152
  * // Fetch will automatically route to the correct snap
@@ -80,81 +158,122 @@ const defaultSnapState = {
80
158
  */
81
159
  export class SnapDataSource extends AbstractDataSource {
82
160
  constructor(options) {
161
+ const configuredNetworks = options.configuredNetworks ?? ALL_DEFAULT_NETWORKS;
83
162
  super(SNAP_DATA_SOURCE_NAME, {
84
163
  ...defaultSnapState,
85
164
  ...options.state,
165
+ activeChains: configuredNetworks,
86
166
  });
87
167
  _SnapDataSource_instances.add(this);
88
168
  _SnapDataSource_messenger.set(this, void 0);
89
- /** Bound handler for snap keyring balance updates, stored for cleanup */
90
- _SnapDataSource_handleSnapBalancesUpdatedBound.set(this, void 0);
91
- _SnapDataSource_handlePermissionStateChangeBound.set(this, void 0);
92
- /** Cache of KeyringClient instances per snap ID to avoid re-instantiation */
93
- _SnapDataSource_keyringClientCache.set(this, new Map());
169
+ _SnapDataSource_snapProvider.set(this, void 0);
94
170
  __classPrivateFieldSet(this, _SnapDataSource_messenger, options.messenger, "f");
95
- // Bind handlers for cleanup in destroy()
96
- __classPrivateFieldSet(this, _SnapDataSource_handleSnapBalancesUpdatedBound, __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_handleSnapBalancesUpdated).bind(this), "f");
97
- __classPrivateFieldSet(this, _SnapDataSource_handlePermissionStateChangeBound, __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_discoverKeyringSnaps).bind(this), "f");
171
+ __classPrivateFieldSet(this, _SnapDataSource_snapProvider, options.snapProvider, "f");
98
172
  __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_registerActionHandlers).call(this);
99
- __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_subscribeToEvents).call(this);
100
- // Discover keyring-capable snaps and populate activeChains dynamically
101
- __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_discoverKeyringSnaps).call(this);
173
+ __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_subscribeToSnapKeyringEvents).call(this);
174
+ // Check availability for all snaps on initialization
175
+ __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_checkAllSnapsAvailability).call(this).catch(() => {
176
+ // Silently ignore availability check failures on init
177
+ });
178
+ }
179
+ /**
180
+ * Get info about all snaps.
181
+ *
182
+ * @returns Record of snap info keyed by snap type.
183
+ */
184
+ getSnapsInfo() {
185
+ const result = {};
186
+ for (const [snapType, config] of Object.entries(SNAP_REGISTRY)) {
187
+ const state = this.state.snaps[snapType];
188
+ result[snapType] = {
189
+ ...config,
190
+ version: state.version,
191
+ available: state.available,
192
+ };
193
+ }
194
+ return result;
195
+ }
196
+ /**
197
+ * Check if a specific snap is available.
198
+ *
199
+ * @param snapType - The snap type to check (solana, bitcoin, tron).
200
+ * @returns True if the snap is available.
201
+ */
202
+ isSnapAvailable(snapType) {
203
+ return this.state.snaps[snapType]?.available ?? false;
204
+ }
205
+ /**
206
+ * Force refresh snap availability check.
207
+ */
208
+ async refreshSnapsStatus() {
209
+ await __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_checkAllSnapsAvailability).call(this);
102
210
  }
103
211
  // ============================================================================
104
- // FETCH
212
+ // CHAIN MANAGEMENT
213
+ // ============================================================================
214
+ addNetworks(chainIds) {
215
+ const snapChains = chainIds.filter(isSnapSupportedChain);
216
+ const newChains = snapChains.filter((chain) => !this.state.activeChains.includes(chain));
217
+ if (newChains.length > 0) {
218
+ const updated = [...this.state.activeChains, ...newChains];
219
+ this.updateActiveChains(updated, (updatedChains) => __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").call('AssetsController:activeChainsUpdate', SNAP_DATA_SOURCE_NAME, updatedChains));
220
+ }
221
+ }
222
+ removeNetworks(chainIds) {
223
+ const chainSet = new Set(chainIds);
224
+ const updated = this.state.activeChains.filter((chain) => !chainSet.has(chain));
225
+ if (updated.length !== this.state.activeChains.length) {
226
+ this.updateActiveChains(updated, (updatedChains) => __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").call('AssetsController:activeChainsUpdate', SNAP_DATA_SOURCE_NAME, updatedChains));
227
+ }
228
+ }
229
+ // ============================================================================
230
+ // FETCH - Routes to appropriate snap(s)
105
231
  // ============================================================================
106
232
  async fetch(request) {
107
- var _a;
108
- // Guard against undefined request
109
- // Note: chainIds filtering is done by middleware/subscribe before calling fetch
110
- if (!request?.accounts || !request?.chainIds?.length) {
233
+ // Guard against undefined request or chainIds
234
+ if (!request?.chainIds) {
111
235
  return {};
112
236
  }
113
- const results = {
114
- assetsBalance: {},
115
- assetsMetadata: {},
116
- };
117
- // Fetch balances for each account using its snap ID from metadata
118
- for (const account of request.accounts) {
119
- // Skip accounts without snap metadata (non-snap accounts)
120
- const snapId = account.metadata.snap?.id;
121
- if (!snapId) {
122
- continue;
237
+ // Filter to only snap-supported chains
238
+ const snapChains = request.chainIds.filter(isSnapSupportedChain);
239
+ if (snapChains.length === 0) {
240
+ return {};
241
+ }
242
+ // Group chains by snap type
243
+ const chainsBySnap = __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_groupChainsBySnap).call(this, snapChains);
244
+ // Fetch from each snap in parallel
245
+ const results = await Promise.all(Object.entries(chainsBySnap).map(async ([snapType, chains]) => {
246
+ return __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_fetchFromSnap).call(this, snapType, {
247
+ ...request,
248
+ chainIds: chains,
249
+ });
250
+ }));
251
+ // Merge all results
252
+ const mergedResponse = {};
253
+ for (const result of results) {
254
+ if (result.assetsBalance) {
255
+ mergedResponse.assetsBalance = {
256
+ ...mergedResponse.assetsBalance,
257
+ ...result.assetsBalance,
258
+ };
123
259
  }
124
- // Skip accounts whose snap doesn't support any of the requested chains
125
- const snapSupportsRequestedChains = request.chainIds.some((chainId) => this.state.chainToSnap[chainId] === snapId);
126
- if (!snapSupportsRequestedChains) {
127
- continue;
260
+ if (result.assetsMetadata) {
261
+ mergedResponse.assetsMetadata = {
262
+ ...mergedResponse.assetsMetadata,
263
+ ...result.assetsMetadata,
264
+ };
128
265
  }
129
- const accountId = account.id;
130
- try {
131
- const client = __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_getKeyringClient).call(this, snapId);
132
- // Step 1: Get the list of assets for this account
133
- const accountAssets = await client.listAccountAssets(accountId);
134
- // If no assets, skip to next account
135
- if (!accountAssets || accountAssets.length === 0) {
136
- continue;
137
- }
138
- // Step 2: Get balances for those specific assets
139
- const balances = await client.getAccountBalances(accountId, accountAssets);
140
- // Transform keyring response to DataResponse format
141
- if (balances && typeof balances === 'object' && results.assetsBalance) {
142
- for (const [assetId, balance] of Object.entries(balances)) {
143
- (_a = results.assetsBalance)[accountId] ?? (_a[accountId] = {});
144
- const accountBalances = results.assetsBalance[accountId];
145
- if (accountBalances) {
146
- accountBalances[assetId] = {
147
- amount: balance.amount,
148
- };
149
- }
150
- }
151
- }
266
+ if (result.assetsPrice) {
267
+ mergedResponse.assetsPrice = {
268
+ ...mergedResponse.assetsPrice,
269
+ ...result.assetsPrice,
270
+ };
152
271
  }
153
- catch {
154
- // Expected when account doesn't belong to this snap
272
+ if (result.errors) {
273
+ mergedResponse.errors = { ...mergedResponse.errors, ...result.errors };
155
274
  }
156
275
  }
157
- return results;
276
+ return mergedResponse;
158
277
  }
159
278
  // ============================================================================
160
279
  // MIDDLEWARE
@@ -242,19 +361,19 @@ export class SnapDataSource extends AbstractDataSource {
242
361
  if (!request?.chainIds) {
243
362
  return;
244
363
  }
245
- // Filter to chains we have a snap for
246
- const supportedChains = request.chainIds.filter((chainId) => __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_isChainSupportedBySnap).call(this, chainId));
247
- if (supportedChains.length === 0) {
364
+ // Filter to only snap-supported chains
365
+ const snapChains = request.chainIds.filter(isSnapSupportedChain);
366
+ if (snapChains.length === 0) {
248
367
  return;
249
368
  }
250
369
  if (isUpdate) {
251
370
  const existing = this.activeSubscriptions.get(subscriptionId);
252
371
  if (existing) {
253
- existing.chains = supportedChains;
372
+ existing.chains = snapChains;
254
373
  // Do a fetch to get latest data on subscription update
255
374
  this.fetch({
256
375
  ...request,
257
- chainIds: supportedChains,
376
+ chainIds: snapChains,
258
377
  })
259
378
  .then(async (fetchResponse) => {
260
379
  if (Object.keys(fetchResponse.assetsBalance ?? {}).length > 0) {
@@ -276,13 +395,13 @@ export class SnapDataSource extends AbstractDataSource {
276
395
  cleanup: () => {
277
396
  // No timer to clear - we use event-based updates
278
397
  },
279
- chains: supportedChains,
398
+ chains: snapChains,
280
399
  });
281
400
  // Initial fetch to get current balances
282
401
  try {
283
402
  const fetchResponse = await this.fetch({
284
403
  ...request,
285
- chainIds: supportedChains,
404
+ chainIds: snapChains,
286
405
  });
287
406
  if (Object.keys(fetchResponse.assetsBalance ?? {}).length > 0) {
288
407
  await __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").call('AssetsController:assetsUpdate', fetchResponse, SNAP_DATA_SOURCE_NAME);
@@ -296,68 +415,52 @@ export class SnapDataSource extends AbstractDataSource {
296
415
  // CLEANUP
297
416
  // ============================================================================
298
417
  destroy() {
299
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
300
- const messenger = __classPrivateFieldGet(this, _SnapDataSource_messenger, "f");
301
- // Unsubscribe from snap keyring events
302
- try {
303
- messenger.unsubscribe('AccountsController:accountBalancesUpdated', __classPrivateFieldGet(this, _SnapDataSource_handleSnapBalancesUpdatedBound, "f"));
304
- }
305
- catch (error) {
306
- log('Failed to unsubscribe from snap keyring events', { error });
307
- }
308
- // Unsubscribe from permission changes
309
- try {
310
- messenger.unsubscribe('PermissionController:stateChange', __classPrivateFieldGet(this, _SnapDataSource_handlePermissionStateChangeBound, "f"));
311
- }
312
- catch (error) {
313
- log('Failed to unsubscribe from permission changes', { error });
314
- }
315
- // Clean up active subscriptions
316
418
  for (const [subscriptionId] of this.activeSubscriptions) {
317
419
  this.unsubscribe(subscriptionId).catch(() => {
318
420
  // Ignore cleanup errors
319
421
  });
320
422
  }
321
- // Clear keyring client cache
322
- __classPrivateFieldGet(this, _SnapDataSource_keyringClientCache, "f").clear();
323
423
  }
324
424
  }
325
- _SnapDataSource_messenger = new WeakMap(), _SnapDataSource_handleSnapBalancesUpdatedBound = new WeakMap(), _SnapDataSource_handlePermissionStateChangeBound = new WeakMap(), _SnapDataSource_keyringClientCache = new WeakMap(), _SnapDataSource_instances = new WeakSet(), _SnapDataSource_subscribeToEvents = function _SnapDataSource_subscribeToEvents() {
326
- // Subscribe to snap keyring events for real-time balance updates.
327
- // The snaps emit AccountBalancesUpdated events when balances change,
328
- // which are re-published by AccountsController.
329
- __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").subscribe('AccountsController:accountBalancesUpdated', __classPrivateFieldGet(this, _SnapDataSource_handleSnapBalancesUpdatedBound, "f"));
330
- // Subscribe to permission changes to detect new keyring snaps at runtime.
331
- // Re-runs snap discovery when permissions change.
332
- __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").subscribe('PermissionController:stateChange', __classPrivateFieldGet(this, _SnapDataSource_handlePermissionStateChangeBound, "f"));
425
+ _SnapDataSource_messenger = new WeakMap(), _SnapDataSource_snapProvider = new WeakMap(), _SnapDataSource_instances = new WeakSet(), _SnapDataSource_subscribeToSnapKeyringEvents = function _SnapDataSource_subscribeToSnapKeyringEvents() {
426
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
427
+ const messenger = __classPrivateFieldGet(this, _SnapDataSource_messenger, "f");
428
+ try {
429
+ messenger.subscribe('AccountsController:accountBalancesUpdated', (payload) => {
430
+ __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_handleSnapBalancesUpdated).call(this, payload);
431
+ });
432
+ }
433
+ catch (error) {
434
+ log('Failed to subscribe to snap keyring events', { error });
435
+ }
333
436
  }, _SnapDataSource_handleSnapBalancesUpdated = function _SnapDataSource_handleSnapBalancesUpdated(payload) {
334
437
  // Transform the snap keyring payload to DataResponse format
335
- let assetsBalance;
438
+ const response = {
439
+ assetsBalance: {},
440
+ };
336
441
  for (const [accountId, assets] of Object.entries(payload.balances)) {
337
- let accountAssets;
338
- for (const [assetId, balance] of Object.entries(assets)) {
339
- const chainId = extractChainFromAssetId(assetId);
340
- if (__classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_isChainSupportedBySnap).call(this, chainId)) {
341
- accountAssets ?? (accountAssets = {});
342
- accountAssets[assetId] = {
343
- amount: balance.amount,
344
- };
442
+ if (response.assetsBalance) {
443
+ response.assetsBalance[accountId] = {};
444
+ for (const [assetId, balance] of Object.entries(assets)) {
445
+ // Only include snap-supported assets (solana, bitcoin, tron)
446
+ if (isSnapSupportedChain(extractChainFromAssetId(assetId))) {
447
+ response.assetsBalance[accountId][assetId] = {
448
+ amount: balance.amount,
449
+ };
450
+ }
451
+ }
452
+ // Remove account if no snap assets
453
+ if (Object.keys(response.assetsBalance[accountId]).length === 0) {
454
+ delete response.assetsBalance[accountId];
345
455
  }
346
- }
347
- if (accountAssets) {
348
- assetsBalance ?? (assetsBalance = {});
349
- assetsBalance[accountId] = accountAssets;
350
456
  }
351
457
  }
352
458
  // Only report if we have snap-related updates
353
- if (assetsBalance) {
354
- const response = { assetsBalance };
459
+ if (Object.keys(response.assetsBalance ?? {}).length > 0) {
355
460
  __classPrivateFieldGet(this, _SnapDataSource_messenger, "f")
356
461
  .call('AssetsController:assetsUpdate', response, SNAP_DATA_SOURCE_NAME)
357
462
  .catch(console.error);
358
463
  }
359
- }, _SnapDataSource_isChainSupportedBySnap = function _SnapDataSource_isChainSupportedBySnap(chainId) {
360
- return this.state.activeChains.includes(chainId);
361
464
  }, _SnapDataSource_registerActionHandlers = function _SnapDataSource_registerActionHandlers() {
362
465
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
363
466
  const messenger = __classPrivateFieldGet(this, _SnapDataSource_messenger, "f");
@@ -366,86 +469,250 @@ _SnapDataSource_messenger = new WeakMap(), _SnapDataSource_handleSnapBalancesUpd
366
469
  messenger.registerActionHandler('SnapDataSource:fetch', async (request) => this.fetch(request));
367
470
  messenger.registerActionHandler('SnapDataSource:subscribe', async (request) => this.subscribe(request));
368
471
  messenger.registerActionHandler('SnapDataSource:unsubscribe', async (subscriptionId) => this.unsubscribe(subscriptionId));
369
- }, _SnapDataSource_getRunnableSnaps = function _SnapDataSource_getRunnableSnaps() {
472
+ }, _SnapDataSource_getInstalledSnaps =
473
+ // ============================================================================
474
+ // SNAP AVAILABILITY
475
+ // ============================================================================
476
+ /**
477
+ * Get all installed snaps from the snap provider.
478
+ *
479
+ * @returns A map of snap IDs to their versions.
480
+ */
481
+ async function _SnapDataSource_getInstalledSnaps() {
370
482
  try {
371
- return __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").call('SnapController:getRunnableSnaps');
483
+ const snaps = await __classPrivateFieldGet(this, _SnapDataSource_snapProvider, "f").request({
484
+ method: 'wallet_getSnaps',
485
+ params: {},
486
+ });
487
+ return snaps;
372
488
  }
373
489
  catch (error) {
374
- log('Failed to get runnable snaps', error);
375
- return [];
490
+ log('Failed to get installed snaps', error);
491
+ return {};
376
492
  }
377
- }, _SnapDataSource_getSnapPermissions = function _SnapDataSource_getSnapPermissions(snapId) {
493
+ }, _SnapDataSource_checkSnapAvailabilityOnDemand =
494
+ /**
495
+ * Check availability for a single snap type on-demand.
496
+ * This is called before each fetch to ensure we have the latest availability status.
497
+ *
498
+ * @param snapType - The snap type to check (solana, bitcoin, tron)
499
+ * @returns True if the snap is available, false otherwise
500
+ */
501
+ async function _SnapDataSource_checkSnapAvailabilityOnDemand(snapType) {
502
+ const config = SNAP_REGISTRY[snapType];
503
+ const currentState = this.state.snaps[snapType];
504
+ // If already marked as available, return true (snap was found previously)
505
+ if (currentState.available) {
506
+ return true;
507
+ }
508
+ // Check if snap is now available (handles timing issues where snap wasn't ready at init)
378
509
  try {
379
- return __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").call('PermissionController:getPermissions', snapId);
510
+ const snaps = await __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_getInstalledSnaps).call(this);
511
+ const snap = snaps[config.snapId];
512
+ if (snap) {
513
+ // Snap is now available - update state
514
+ this.state.snaps[snapType] = {
515
+ version: snap.version,
516
+ available: true,
517
+ };
518
+ return true;
519
+ }
520
+ return false;
380
521
  }
381
- catch (error) {
382
- log('Failed to get permissions for snap', { snapId, error });
383
- return undefined;
522
+ catch {
523
+ return false;
384
524
  }
385
- }, _SnapDataSource_discoverKeyringSnaps = function _SnapDataSource_discoverKeyringSnaps() {
525
+ }, _SnapDataSource_checkAllSnapsAvailability = async function _SnapDataSource_checkAllSnapsAvailability() {
386
526
  try {
387
- const runnableSnaps = __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_getRunnableSnaps).call(this);
388
- const chainToSnap = {};
389
- const supportedChains = [];
390
- for (const snap of runnableSnaps) {
391
- const permissions = __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_getSnapPermissions).call(this, snap.id);
392
- // Must have endowment:keyring permission to be a keyring snap
393
- if (!permissions?.[KEYRING_PERMISSION]) {
394
- continue;
527
+ const snaps = await __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_getInstalledSnaps).call(this);
528
+ for (const [snapType, config] of Object.entries(SNAP_REGISTRY)) {
529
+ const snap = snaps[config.snapId];
530
+ if (snap) {
531
+ this.state.snaps[snapType] = {
532
+ version: snap.version,
533
+ available: true,
534
+ };
395
535
  }
396
- // Get chainIds caveat from the assets permission (not keyring permission)
397
- // The chainIds are stored in endowment:assets
398
- const assetsPermission = permissions[ASSETS_PERMISSION];
399
- const chainIds = getChainIdsCaveat(assetsPermission);
400
- // Map each chain to this snap (first snap wins if multiple support same chain)
401
- if (chainIds) {
402
- for (const chainId of chainIds) {
403
- if (!(chainId in chainToSnap)) {
404
- chainToSnap[chainId] = snap.id;
405
- supportedChains.push(chainId);
406
- }
407
- }
536
+ else {
537
+ this.state.snaps[snapType] = {
538
+ version: null,
539
+ available: false,
540
+ };
408
541
  }
409
542
  }
410
- // Update chainToSnap mapping
411
- this.state.chainToSnap = chainToSnap;
412
- // Notify if chains changed
413
- try {
414
- this.updateActiveChains(supportedChains, (updatedChains) => {
415
- __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").call('AssetsController:activeChainsUpdate', SNAP_DATA_SOURCE_NAME, updatedChains);
416
- });
543
+ }
544
+ catch {
545
+ // Mark all snaps as unavailable on error
546
+ for (const snapType of Object.keys(SNAP_REGISTRY)) {
547
+ this.state.snaps[snapType] = {
548
+ version: null,
549
+ available: false,
550
+ };
417
551
  }
418
- catch {
419
- // AssetsController not ready yet - expected during initialization
552
+ }
553
+ }, _SnapDataSource_accountSupportsChain = function _SnapDataSource_accountSupportsChain(account, chainId) {
554
+ const scopes = account.scopes ?? [];
555
+ // If no scopes defined, assume it supports the chain (backward compatibility)
556
+ if (scopes.length === 0) {
557
+ return true;
558
+ }
559
+ // Extract namespace and reference from chainId
560
+ const [chainNamespace, chainReference] = chainId.split(':');
561
+ for (const scope of scopes) {
562
+ const [scopeNamespace, scopeReference] = scope.split(':');
563
+ // Check if namespaces match
564
+ if (scopeNamespace !== chainNamespace) {
565
+ continue;
566
+ }
567
+ // Wildcard scope (e.g., "solana:0" means all chains in that namespace)
568
+ if (scopeReference === '0') {
569
+ return true;
570
+ }
571
+ // Exact match check
572
+ if (scopeReference === chainReference) {
573
+ return true;
420
574
  }
421
575
  }
422
- catch (error) {
423
- log('Keyring snap discovery failed', { error });
424
- this.state.chainToSnap = {};
576
+ return false;
577
+ }, _SnapDataSource_groupChainsBySnap = function _SnapDataSource_groupChainsBySnap(chainIds) {
578
+ const groups = {};
579
+ for (const chainId of chainIds) {
580
+ const snapType = getSnapTypeForChain(chainId);
581
+ if (snapType) {
582
+ groups[snapType] ?? (groups[snapType] = []);
583
+ const snapChains = groups[snapType];
584
+ if (snapChains) {
585
+ snapChains.push(chainId);
586
+ }
587
+ }
588
+ }
589
+ return groups;
590
+ }, _SnapDataSource_fetchFromSnap = async function _SnapDataSource_fetchFromSnap(snapType, request) {
591
+ var _a;
592
+ const config = SNAP_REGISTRY[snapType];
593
+ // Check snap availability on-demand - handles timing issues where snap
594
+ // wasn't ready during initialization but is now available
595
+ const isAvailable = await __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_checkSnapAvailabilityOnDemand).call(this, snapType);
596
+ if (!isAvailable) {
597
+ log(`${snapType} snap not available, skipping fetch`);
598
+ // Return errors for these chains so they can fallback to other data sources
599
+ const errors = {};
600
+ for (const chainId of request.chainIds) {
601
+ errors[chainId] = `${snapType} snap not available`;
602
+ }
603
+ return { errors };
604
+ }
605
+ const results = {
606
+ assetsBalance: {},
607
+ assetsMetadata: {},
608
+ };
609
+ // Fetch balances for each account using Keyring API
610
+ // Important: Must first get account assets, then request balances for those specific assets
611
+ for (const account of request.accounts) {
612
+ // Filter to only process accounts that support the chains being fetched
613
+ const accountSupportedChains = request.chainIds.filter((chainId) => __classPrivateFieldGet(this, _SnapDataSource_instances, "m", _SnapDataSource_accountSupportsChain).call(this, account, chainId));
614
+ // Skip accounts that don't support any of the requested chains
615
+ if (accountSupportedChains.length === 0) {
616
+ continue;
617
+ }
618
+ const accountId = account.id;
425
619
  try {
426
- this.updateActiveChains([], (updatedChains) => {
427
- __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").call('AssetsController:activeChainsUpdate', SNAP_DATA_SOURCE_NAME, updatedChains);
620
+ // Step 1: Get the list of assets for this account
621
+ log(`${snapType} snap calling keyring_listAccountAssets`, {
622
+ snapId: config.snapId,
623
+ accountId,
624
+ });
625
+ const accountAssets = await __classPrivateFieldGet(this, _SnapDataSource_snapProvider, "f").request({
626
+ method: 'wallet_invokeSnap',
627
+ params: {
628
+ snapId: config.snapId,
629
+ request: {
630
+ method: 'keyring_listAccountAssets',
631
+ params: {
632
+ id: accountId, // Account UUID
633
+ },
634
+ },
635
+ },
636
+ });
637
+ log(`${snapType} snap keyring_listAccountAssets response`, {
638
+ accountId,
639
+ assetCount: accountAssets?.length ?? 0,
640
+ assets: accountAssets,
641
+ });
642
+ // If no assets, skip to next account
643
+ if (!accountAssets || accountAssets.length === 0) {
644
+ log(`${snapType} snap: account has no assets, skipping balance fetch`, {
645
+ accountId,
646
+ });
647
+ continue;
648
+ }
649
+ // Step 2: Get balances for those specific assets
650
+ log(`${snapType} snap calling keyring_getAccountBalances`, {
651
+ snapId: config.snapId,
652
+ accountId,
653
+ requestedAssets: accountAssets.length,
654
+ });
655
+ const balances = await __classPrivateFieldGet(this, _SnapDataSource_snapProvider, "f").request({
656
+ method: 'wallet_invokeSnap',
657
+ params: {
658
+ snapId: config.snapId,
659
+ request: {
660
+ method: 'keyring_getAccountBalances',
661
+ params: {
662
+ id: accountId, // Account UUID (the keyring API uses 'id' not 'accountId')
663
+ assets: accountAssets, // Must pass specific asset types from listAccountAssets
664
+ },
665
+ },
666
+ },
667
+ });
668
+ log(`${snapType} snap keyring_getAccountBalances response`, {
669
+ accountId,
670
+ balances,
671
+ balancesType: typeof balances,
672
+ isNull: balances === null,
673
+ isUndefined: balances === undefined,
674
+ assetCount: balances ? Object.keys(balances).length : 0,
428
675
  });
676
+ // Transform keyring response to DataResponse format
677
+ // Note: snap may return null/undefined if account doesn't belong to this snap
678
+ if (balances && typeof balances === 'object' && results.assetsBalance) {
679
+ const balanceEntries = Object.entries(balances);
680
+ log(`${snapType} snap processing ${balanceEntries.length} balances for account ${accountId}`);
681
+ for (const [assetId, balance] of balanceEntries) {
682
+ // Initialize account balances if not exists
683
+ (_a = results.assetsBalance)[accountId] ?? (_a[accountId] = {});
684
+ // Store raw balance for this asset
685
+ // Use rawAmount if available (preferred - smallest unit), fall back to amount
686
+ // Note: Snaps should return rawAmount in smallest unit (satoshis, lamports, etc.)
687
+ const accountBalances = results.assetsBalance[accountId];
688
+ if (accountBalances) {
689
+ accountBalances[assetId] = {
690
+ amount: balance.rawAmount ?? balance.amount,
691
+ };
692
+ }
693
+ }
694
+ }
695
+ else if (!balances) {
696
+ log(`${snapType} snap returned empty/null for account (account may not belong to this snap)`, {
697
+ accountId,
698
+ balances,
699
+ });
700
+ }
429
701
  }
430
- catch {
431
- // AssetsController not ready yet - expected during initialization
702
+ catch (error) {
703
+ // This is expected when querying a snap with an account it doesn't manage
704
+ log(`${snapType} snap fetch FAILED for account`, {
705
+ accountId,
706
+ error: error instanceof Error ? error.message : String(error),
707
+ errorStack: error instanceof Error ? error.stack : undefined,
708
+ });
432
709
  }
433
710
  }
434
- }, _SnapDataSource_getKeyringClient = function _SnapDataSource_getKeyringClient(snapId) {
435
- const cachedClient = __classPrivateFieldGet(this, _SnapDataSource_keyringClientCache, "f").get(snapId);
436
- if (cachedClient) {
437
- return cachedClient;
438
- }
439
- const client = new KeyringClient({
440
- send: async (request) => (await __classPrivateFieldGet(this, _SnapDataSource_messenger, "f").call('SnapController:handleRequest', {
441
- snapId: snapId,
442
- origin: 'metamask',
443
- handler: HandlerType.OnKeyringRequest,
444
- request,
445
- })),
711
+ log(`${snapType} snap fetch completed`, {
712
+ chains: request.chainIds.length,
713
+ accountsWithBalances: Object.keys(results.assetsBalance ?? {}).length,
446
714
  });
447
- __classPrivateFieldGet(this, _SnapDataSource_keyringClientCache, "f").set(snapId, client);
448
- return client;
715
+ return results;
449
716
  };
450
717
  // ============================================================================
451
718
  // FACTORY