@metamask-previews/assets-controller 0.0.0-preview-8a281087 → 0.0.0-preview-69f51f81

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