@metamask-previews/assets-controller 16.1.0-preview-b1e33e9f1 → 16.1.0-preview-d2ea3cc

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 (81) hide show
  1. package/CHANGELOG.md +0 -28
  2. package/dist/AssetsController-method-action-types.d.ts +1 -5
  3. package/dist/AssetsController-method-action-types.d.ts.map +1 -1
  4. package/dist/AssetsController-method-action-types.js.map +1 -1
  5. package/dist/AssetsController.d.ts +40 -8
  6. package/dist/AssetsController.d.ts.map +1 -1
  7. package/dist/AssetsController.js +235 -570
  8. package/dist/AssetsController.js.map +1 -1
  9. package/dist/data-sources/AbstractDataSource.d.ts +6 -1
  10. package/dist/data-sources/AbstractDataSource.d.ts.map +1 -1
  11. package/dist/data-sources/AbstractDataSource.js.map +1 -1
  12. package/dist/data-sources/AccountsApiDataSource.d.ts +3 -17
  13. package/dist/data-sources/AccountsApiDataSource.d.ts.map +1 -1
  14. package/dist/data-sources/AccountsApiDataSource.js +125 -275
  15. package/dist/data-sources/AccountsApiDataSource.js.map +1 -1
  16. package/dist/data-sources/PriceDataSource.d.ts +6 -7
  17. package/dist/data-sources/PriceDataSource.d.ts.map +1 -1
  18. package/dist/data-sources/PriceDataSource.js +20 -15
  19. package/dist/data-sources/PriceDataSource.js.map +1 -1
  20. package/dist/data-sources/RpcDataSource.d.ts +1 -15
  21. package/dist/data-sources/RpcDataSource.d.ts.map +1 -1
  22. package/dist/data-sources/RpcDataSource.js +107 -398
  23. package/dist/data-sources/RpcDataSource.js.map +1 -1
  24. package/dist/data-sources/SnapDataSource.d.ts +0 -5
  25. package/dist/data-sources/SnapDataSource.d.ts.map +1 -1
  26. package/dist/data-sources/SnapDataSource.js +0 -114
  27. package/dist/data-sources/SnapDataSource.js.map +1 -1
  28. package/dist/data-sources/TokenDataSource.d.ts +1 -3
  29. package/dist/data-sources/TokenDataSource.d.ts.map +1 -1
  30. package/dist/data-sources/TokenDataSource.js +2 -4
  31. package/dist/data-sources/TokenDataSource.js.map +1 -1
  32. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.ts +0 -8
  33. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.ts.map +1 -1
  34. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.js +2 -47
  35. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/middlewares/CustomAssetGraduationMiddleware.d.ts +1 -3
  40. package/dist/middlewares/CustomAssetGraduationMiddleware.d.ts.map +1 -1
  41. package/dist/middlewares/CustomAssetGraduationMiddleware.js +1 -3
  42. package/dist/middlewares/CustomAssetGraduationMiddleware.js.map +1 -1
  43. package/dist/middlewares/DetectionMiddleware.d.ts +2 -10
  44. package/dist/middlewares/DetectionMiddleware.d.ts.map +1 -1
  45. package/dist/middlewares/DetectionMiddleware.js +23 -5
  46. package/dist/middlewares/DetectionMiddleware.js.map +1 -1
  47. package/dist/middlewares/ParallelMiddleware.d.ts.map +1 -1
  48. package/dist/middlewares/ParallelMiddleware.js +3 -0
  49. package/dist/middlewares/ParallelMiddleware.js.map +1 -1
  50. package/dist/middlewares/RpcFallbackMiddleware.d.ts +15 -16
  51. package/dist/middlewares/RpcFallbackMiddleware.d.ts.map +1 -1
  52. package/dist/middlewares/RpcFallbackMiddleware.js +107 -125
  53. package/dist/middlewares/RpcFallbackMiddleware.js.map +1 -1
  54. package/dist/middlewares/index.d.ts +0 -1
  55. package/dist/middlewares/index.d.ts.map +1 -1
  56. package/dist/middlewares/index.js.map +1 -1
  57. package/dist/migrations/healAssetsInfoMetadata.d.ts +5 -5
  58. package/dist/migrations/healAssetsInfoMetadata.d.ts.map +1 -1
  59. package/dist/migrations/healAssetsInfoMetadata.js.map +1 -1
  60. package/dist/pipeline/buildFastFetchSources.d.ts +0 -5
  61. package/dist/pipeline/buildFastFetchSources.d.ts.map +1 -1
  62. package/dist/pipeline/buildFastFetchSources.js +1 -7
  63. package/dist/pipeline/buildFastFetchSources.js.map +1 -1
  64. package/dist/pipeline/executeAssetsPipeline.d.ts +3 -1
  65. package/dist/pipeline/executeAssetsPipeline.d.ts.map +1 -1
  66. package/dist/pipeline/executeAssetsPipeline.js +2 -1
  67. package/dist/pipeline/executeAssetsPipeline.js.map +1 -1
  68. package/dist/selectors/balance.js +1 -1
  69. package/dist/selectors/balance.js.map +1 -1
  70. package/dist/types.d.ts +23 -29
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js.map +1 -1
  73. package/package.json +1 -1
  74. package/dist/utils/assetVisibility.d.ts +0 -29
  75. package/dist/utils/assetVisibility.d.ts.map +0 -1
  76. package/dist/utils/assetVisibility.js +0 -80
  77. package/dist/utils/assetVisibility.js.map +0 -1
  78. package/dist/utils/filterFailedChainBalances.d.ts +0 -11
  79. package/dist/utils/filterFailedChainBalances.d.ts.map +0 -1
  80. package/dist/utils/filterFailedChainBalances.js +0 -22
  81. package/dist/utils/filterFailedChainBalances.js.map +0 -1
@@ -1,13 +1,12 @@
1
1
  import { Web3Provider } from '@ethersproject/providers';
2
2
  import { toHex } from '@metamask/controller-utils';
3
- import { isStrictHexString, isCaipChainId, KnownCaipNamespace, parseCaipAssetType, parseCaipChainId, hexToNumber, toCaipChainId, } from '@metamask/utils';
3
+ import { isStrictHexString, isCaipChainId, parseCaipAssetType, parseCaipChainId, hexToNumber, toCaipChainId, KnownCaipNamespace, } from '@metamask/utils';
4
4
  import { BigNumber as BigNumberJS } from 'bignumber.js';
5
5
  import { projectLogger, createModuleLogger } from '../logger.js';
6
6
  import { ZERO_ADDRESS } from '../utils/constants.js';
7
- import { filterFailedChainBalances } from '../utils/filterFailedChainBalances.js';
8
7
  import { normalizeAssetId } from '../utils/index.js';
9
8
  import { AbstractDataSource } from './AbstractDataSource.js';
10
- import { BalanceFetcher, isStakingContractAssetId, MulticallClient, TokenDetector, TokensApiClient, } from './evm-rpc-services/index.js';
9
+ import { BalanceFetcher, MulticallClient, TokenDetector, TokensApiClient, } from './evm-rpc-services/index.js';
11
10
  import { shouldSkipNativeForCaipChainId } from './evm-rpc-services/utils/assets.js';
12
11
  const CONTROLLER_NAME = 'RpcDataSource';
13
12
  const DEFAULT_BALANCE_INTERVAL = 30_000; // 30 seconds
@@ -48,7 +47,6 @@ export const caipChainIdToHex = (chainId) => {
48
47
  */
49
48
  export class RpcDataSource extends AbstractDataSource {
50
49
  #messenger;
51
- #getAssetsState;
52
50
  #onActiveChainsUpdated;
53
51
  #getNativeAssetForChain;
54
52
  #timeout;
@@ -69,12 +67,9 @@ export class RpcDataSource extends AbstractDataSource {
69
67
  #balanceFetcher;
70
68
  #tokenDetector;
71
69
  #getAssetType;
72
- #isBalanceV6Enabled;
73
- #getAssetVisibility;
74
70
  constructor(options) {
75
71
  super(CONTROLLER_NAME, { activeChains: [] });
76
72
  this.#messenger = options.messenger;
77
- this.#getAssetsState = options.getAssetsState;
78
73
  this.#onActiveChainsUpdated = options.onActiveChainsUpdated;
79
74
  this.#getNativeAssetForChain = options.getNativeAssetForChain;
80
75
  this.#getAssetType = options.getAssetType;
@@ -84,9 +79,6 @@ export class RpcDataSource extends AbstractDataSource {
84
79
  this.#useExternalService =
85
80
  options.useExternalService ?? (() => true);
86
81
  this.#isOnboarded = options.isOnboarded ?? (() => true);
87
- this.#isBalanceV6Enabled =
88
- options.isBalanceV6Enabled ?? (() => false);
89
- this.#getAssetVisibility = options.getAssetVisibility;
90
82
  const balanceInterval = options.balanceInterval ?? DEFAULT_BALANCE_INTERVAL;
91
83
  const detectionInterval = options.detectionInterval ?? DEFAULT_DETECTION_INTERVAL;
92
84
  log('Initializing RpcDataSource', {
@@ -126,8 +118,6 @@ export class RpcDataSource extends AbstractDataSource {
126
118
  const nativeId = this.#getNativeAssetForChain(chainId);
127
119
  return nativeId?.toLowerCase() === assetId.toLowerCase();
128
120
  },
129
- isBalanceV6Enabled: () => this.#isBalanceV6Enabled(),
130
- getAssetVisibility: this.#getAssetVisibility,
131
121
  });
132
122
  // Polling controller awaits this callback; rejections must not become unhandled.
133
123
  this.#balanceFetcher.setOnBalanceUpdate(async (result) => {
@@ -273,19 +263,6 @@ export class RpcDataSource extends AbstractDataSource {
273
263
  * @param result - The balance fetch result.
274
264
  */
275
265
  async #handleBalanceUpdate(result) {
276
- if (this.#isBalanceV6Enabled()) {
277
- await this.#handleBalanceUpdateV6(result);
278
- return;
279
- }
280
- await this.#handleBalanceUpdateV5(result);
281
- }
282
- /**
283
- * v5 poll path. Unchanged from the pre-v6 handler; delete the v6 sibling
284
- * first if `assetsAccountsApiV6` is rolled back.
285
- *
286
- * @param result - The balance fetch result.
287
- */
288
- async #handleBalanceUpdateV5(result) {
289
266
  const newBalances = {};
290
267
  // Convert hex chain ID to CAIP-2 format
291
268
  const chainIdDecimal = parseInt(result.chainId, 16);
@@ -336,75 +313,6 @@ export class RpcDataSource extends AbstractDataSource {
336
313
  });
337
314
  }
338
315
  }
339
- /**
340
- * v6 poll path: a complete chain snapshot, or nothing. Delete with the rest
341
- * of the v6 path if `assetsAccountsApiV6` is rolled back.
342
- *
343
- * @param result - The balance fetch result.
344
- */
345
- async #handleBalanceUpdateV6(result) {
346
- if (result.failedAddresses.length > 0) {
347
- log('Skipping incomplete v6 poll snapshot', {
348
- accountId: result.accountId,
349
- chainId: result.chainId,
350
- failedAddresses: result.failedAddresses,
351
- });
352
- return;
353
- }
354
- const newBalances = {};
355
- // Convert hex chain ID to CAIP-2 format
356
- const chainIdDecimal = parseInt(result.chainId, 16);
357
- const caipChainId = `eip155:${chainIdDecimal}`;
358
- // Normalize asset IDs from BalanceFetcher (lowercase) to checksummed form
359
- const normalizedBalances = result.balances.map((b) => ({
360
- ...b,
361
- assetId: normalizeAssetId(b.assetId),
362
- }));
363
- // Collect metadata for all balances
364
- const assetsInfo = this.#collectMetadataForBalances(normalizedBalances, caipChainId);
365
- // Convert balances to human-readable format.
366
- // Unknown decimals mean the snapshot is incomplete: drop the chain
367
- // instead of publishing a partial `full` result.
368
- const existingMetadata = this.#getExistingAssetsMetadata();
369
- for (const balance of normalizedBalances) {
370
- const stateMetadata = existingMetadata[balance.assetId];
371
- const pipelineMetadata = assetsInfo[balance.assetId];
372
- const decimals = this.#pickValidDecimals(stateMetadata, pipelineMetadata);
373
- if (decimals === undefined) {
374
- log('Skipping incomplete v6 poll snapshot', {
375
- accountId: result.accountId,
376
- chainId: result.chainId,
377
- assetId: balance.assetId,
378
- });
379
- return;
380
- }
381
- const humanReadableAmount = this.#convertToHumanReadable(balance.balance, decimals);
382
- newBalances[balance.assetId] = {
383
- amount: humanReadableAmount,
384
- };
385
- }
386
- const response = {
387
- assetsBalance: {
388
- [result.accountId]: newBalances,
389
- },
390
- assetsInfo,
391
- updateMode: 'full',
392
- };
393
- const request = {
394
- accountsWithSupportedChains: [],
395
- chainIds: [caipChainId],
396
- dataTypes: ['balance'],
397
- };
398
- log('Balance update response', {
399
- accountId: result.accountId,
400
- newBalanceCount: Object.keys(newBalances).length,
401
- });
402
- for (const subscription of this.#activeSubscriptions.values()) {
403
- subscription.onAssetsUpdate(response, request)?.catch((error) => {
404
- log('Failed to update assets', { error });
405
- });
406
- }
407
- }
408
316
  /**
409
317
  * Handle detection update from TokenDetector.
410
318
  *
@@ -753,80 +661,21 @@ export class RpcDataSource extends AbstractDataSource {
753
661
  log('Skipping fetch - onboarding not complete');
754
662
  return {};
755
663
  }
756
- if (this.#isBalanceV6Enabled()) {
757
- return this.#fetchV6(request);
758
- }
759
- return this.#fetchV5(request);
760
- }
761
- /**
762
- * v5 fetch. Overlays tokens that succeed, stamps `merge`. Delete with the
763
- * rest of the v5 path if `assetsAccountsApiV6` is rolled back.
764
- *
765
- * @param request - The data request.
766
- * @returns Balance and metadata for the requested chains.
767
- */
768
- async #fetchV5(request) {
769
- const chainsToFetch = this.#getChainsToFetch(request);
770
- if (chainsToFetch.length === 0) {
771
- log('No active chains to fetch');
772
- return {};
773
- }
774
- const assetsBalance = {};
775
- const assetsInfo = {};
776
- const failedChains = [];
777
- for (const { account, supportedChains, } of request.accountsWithSupportedChains) {
778
- const chainsForAccount = chainsToFetch.filter((chain) => supportedChains.includes(chain));
779
- if (chainsForAccount.length === 0) {
780
- continue;
781
- }
782
- const { address, id: accountId } = account;
783
- for (const chainId of chainsForAccount) {
784
- const hexChainId = caipChainIdToHex(chainId);
785
- const nativeAssetId = this.#getNativeAssetForChain(chainId);
786
- const shouldSkipNative = shouldSkipNativeForCaipChainId(chainId);
787
- const assetsToFetch = this.#getAssetsToFetchV5(request, chainId, shouldSkipNative);
788
- try {
789
- const result = await this.#balanceFetcher.fetchBalancesForAssets(hexChainId, accountId, address, assetsToFetch);
790
- await this.#ingestFetchedBalances(result, accountId, chainId, assetsBalance, assetsInfo);
791
- }
792
- catch (error) {
793
- this.#recordFetchChainFailure({
794
- address,
795
- chainId,
796
- error,
797
- accountId,
798
- nativeAssetId,
799
- shouldSkipNative,
800
- assetsBalance,
801
- assetsInfo,
802
- failedChains,
803
- });
804
- }
805
- }
806
- }
807
- return this.#completeFetchResponse({
808
- assetsBalance,
809
- assetsInfo,
810
- failedChains,
664
+ const response = {};
665
+ const chainsToFetch = request.chainIds.filter((chainId) => this.#activeChains.includes(chainId));
666
+ log('Fetch requested', {
667
+ accounts: request.accountsWithSupportedChains.map((a) => a.account.id),
668
+ requestedChains: request.chainIds,
811
669
  chainsToFetch,
812
- updateMode: 'merge',
813
670
  });
814
- }
815
- /**
816
- * v6 fetch. Visibility-scoped snapshot, atomic per chain, stamps `full`.
817
- *
818
- * @param request - The data request.
819
- * @returns Balance and metadata for successful chains only.
820
- */
821
- async #fetchV6(request) {
822
- const chainsToFetch = this.#getChainsToFetch(request);
823
671
  if (chainsToFetch.length === 0) {
824
672
  log('No active chains to fetch');
825
- return {};
673
+ return response;
826
674
  }
827
675
  const assetsBalance = {};
828
676
  const assetsInfo = {};
829
677
  const failedChains = [];
678
+ // Fetch balances for each account and its supported chains (pre-computed in request)
830
679
  for (const { account, supportedChains, } of request.accountsWithSupportedChains) {
831
680
  const chainsForAccount = chainsToFetch.filter((chain) => supportedChains.includes(chain));
832
681
  if (chainsForAccount.length === 0) {
@@ -837,133 +686,100 @@ export class RpcDataSource extends AbstractDataSource {
837
686
  const hexChainId = caipChainIdToHex(chainId);
838
687
  const nativeAssetId = this.#getNativeAssetForChain(chainId);
839
688
  const shouldSkipNative = shouldSkipNativeForCaipChainId(chainId);
840
- try {
841
- const result = await this.#balanceFetcher.fetchBalancesForAssets(hexChainId, accountId, address, this.#getAssetsToFetchV6(accountId, chainId));
842
- // A failed `balanceOf` or unresolved decimals means we do not have
843
- // the full list, so drop this chain instead of applying a partial
844
- // `full` snapshot (which would omit that token from state).
845
- if (result.failedAddresses.length > 0) {
846
- if (!failedChains.includes(chainId)) {
847
- failedChains.push(chainId);
689
+ const assetsToFetch = [];
690
+ if (!shouldSkipNative) {
691
+ // Build a single AssetFetchEntry[] for native + custom ERC-20s
692
+ assetsToFetch.push({ assetId: nativeAssetId, address: ZERO_ADDRESS });
693
+ }
694
+ if (request.customAssets) {
695
+ const existingMetadata = this.#getExistingAssetsMetadata();
696
+ for (const assetId of request.customAssets) {
697
+ try {
698
+ const parsed = parseCaipAssetType(assetId);
699
+ const assetChainId = `${parsed.chain.namespace}:${parsed.chain.reference}`;
700
+ if (assetChainId === chainId &&
701
+ this.#getAssetType(assetId) === 'erc20') {
702
+ const tokenAddress = parsed.assetReference.toLowerCase();
703
+ const normalizedId = normalizeAssetId(assetId);
704
+ const decimals = existingMetadata[normalizedId]?.decimals;
705
+ assetsToFetch.push({
706
+ assetId,
707
+ address: tokenAddress,
708
+ decimals,
709
+ });
710
+ }
848
711
  }
849
- continue;
712
+ catch {
713
+ // Skip unparseable asset IDs
714
+ }
715
+ }
716
+ }
717
+ try {
718
+ const result = await this.#balanceFetcher.fetchBalancesForAssets(hexChainId, accountId, address, assetsToFetch);
719
+ if (!assetsBalance[accountId]) {
720
+ assetsBalance[accountId] = {};
850
721
  }
851
- const ingested = await this.#ingestFetchedBalances(result, accountId, chainId, assetsBalance, assetsInfo, { failOnUnresolvedDecimals: true });
852
- if (!ingested) {
853
- if (!failedChains.includes(chainId)) {
854
- failedChains.push(chainId);
722
+ // Normalize asset IDs from BalanceFetcher (which uses lowercase
723
+ // addresses) to checksummed form so they match assetsInfo state keys.
724
+ const normalizedBalances = result.balances.map((b) => ({
725
+ ...b,
726
+ assetId: normalizeAssetId(b.assetId),
727
+ }));
728
+ // Collect metadata for all balances
729
+ const balanceMetadata = this.#collectMetadataForBalances(normalizedBalances, chainId);
730
+ Object.assign(assetsInfo, balanceMetadata);
731
+ // Convert balances to human-readable format using decimals from
732
+ // assetsInfo state (which includes pendingMetadata from addCustomAsset).
733
+ // Resolution: state → pipeline metadata → RPC `decimals()`; omit balance if still unknown.
734
+ const existingMetadata = this.#getExistingAssetsMetadata();
735
+ for (const balance of normalizedBalances) {
736
+ const stateMetadata = existingMetadata[balance.assetId];
737
+ const pipelineMetadata = assetsInfo[balance.assetId];
738
+ let decimals = this.#pickValidDecimals(stateMetadata, pipelineMetadata);
739
+ if (decimals === undefined) {
740
+ const parsed = parseCaipAssetType(balance.assetId);
741
+ if (this.#getAssetType(balance.assetId) === 'erc20') {
742
+ decimals = await this.#fetchDecimalsViaRpc(chainId, parsed.assetReference);
743
+ }
744
+ }
745
+ if (decimals === undefined) {
746
+ continue;
855
747
  }
748
+ const humanReadableAmount = this.#convertToHumanReadable(balance.balance, decimals);
749
+ assetsBalance[accountId][balance.assetId] = {
750
+ amount: humanReadableAmount,
751
+ };
856
752
  }
857
753
  }
858
754
  catch (error) {
859
- this.#recordFetchChainFailure({
860
- address,
861
- chainId,
862
- error,
863
- accountId,
864
- nativeAssetId,
865
- shouldSkipNative,
866
- assetsBalance,
867
- assetsInfo,
868
- failedChains,
869
- });
755
+ log('Failed to fetch balance', { address, chainId, error });
756
+ if (!assetsBalance[accountId]) {
757
+ assetsBalance[accountId] = {};
758
+ }
759
+ if (!shouldSkipNative) {
760
+ assetsBalance[accountId][nativeAssetId] = { amount: '0' };
761
+ }
762
+ // On error, emit a stub only when no valid metadata exists in state
763
+ // yet. Re-emitting existing metadata would overwrite richer entries
764
+ // (e.g. image/description added by AccountsAPI) with a simpler stub.
765
+ const existingNativeMeta = this.#getExistingAssetsMetadata()[nativeAssetId];
766
+ if (!this.#hasValidDecimals(existingNativeMeta)) {
767
+ const chainStatus = this.#chainStatuses[chainId];
768
+ if (chainStatus) {
769
+ assetsInfo[nativeAssetId] = {
770
+ type: 'native',
771
+ symbol: chainStatus.nativeCurrency,
772
+ name: chainStatus.nativeCurrency,
773
+ decimals: 18,
774
+ };
775
+ }
776
+ }
777
+ if (!failedChains.includes(chainId)) {
778
+ failedChains.push(chainId);
779
+ }
870
780
  }
871
781
  }
872
782
  }
873
- return this.#completeFetchResponse({
874
- // A failed chain contributes nothing, so its balances stay as they are.
875
- assetsBalance: filterFailedChainBalances(assetsBalance, new Set(failedChains)),
876
- assetsInfo,
877
- failedChains,
878
- chainsToFetch,
879
- updateMode: 'full',
880
- });
881
- }
882
- #getChainsToFetch(request) {
883
- const chainsToFetch = request.chainIds.filter((chainId) => this.#activeChains.includes(chainId));
884
- log('Fetch requested', {
885
- accounts: request.accountsWithSupportedChains.map((a) => a.account.id),
886
- requestedChains: request.chainIds,
887
- chainsToFetch,
888
- });
889
- return chainsToFetch;
890
- }
891
- /**
892
- * Convert fetched raw balances into human-readable amounts.
893
- *
894
- * v5 overlays tokens that resolve and skips the rest (`merge`). v6 must not
895
- * publish a partial `full` snapshot: when `failOnUnresolvedDecimals` is set,
896
- * unresolved decimals abort ingest so the caller can fail the chain.
897
- *
898
- * @param result - The balance fetch result.
899
- * @param accountId - The account the balances belong to.
900
- * @param chainId - The CAIP-2 chain ID.
901
- * @param assetsBalance - Accumulator for converted balances.
902
- * @param assetsInfo - Accumulator for metadata collected from this fetch.
903
- * @param options - Ingest options.
904
- * @param options.failOnUnresolvedDecimals - When true, stop ingesting and
905
- * return false if any token's decimals cannot be resolved.
906
- * @returns `false` when ingest was aborted for unresolved decimals; otherwise `true`.
907
- */
908
- async #ingestFetchedBalances(result, accountId, chainId, assetsBalance, assetsInfo, { failOnUnresolvedDecimals = false, } = {}) {
909
- assetsBalance[accountId] ??= {};
910
- const normalizedBalances = result.balances.map((balance) => ({
911
- ...balance,
912
- assetId: normalizeAssetId(balance.assetId),
913
- }));
914
- Object.assign(assetsInfo, this.#collectMetadataForBalances(normalizedBalances, chainId));
915
- const existingMetadata = this.#getExistingAssetsMetadata();
916
- for (const balance of normalizedBalances) {
917
- const stateMetadata = existingMetadata[balance.assetId];
918
- const pipelineMetadata = assetsInfo[balance.assetId];
919
- let decimals = this.#pickValidDecimals(stateMetadata, pipelineMetadata);
920
- if (decimals === undefined) {
921
- const parsed = parseCaipAssetType(balance.assetId);
922
- if (this.#getAssetType(balance.assetId) === 'erc20') {
923
- decimals = await this.#fetchDecimalsViaRpc(chainId, parsed.assetReference);
924
- }
925
- }
926
- if (decimals === undefined) {
927
- if (failOnUnresolvedDecimals) {
928
- log('Skipping incomplete v6 fetch snapshot', {
929
- accountId,
930
- chainId,
931
- assetId: balance.assetId,
932
- });
933
- return false;
934
- }
935
- continue;
936
- }
937
- assetsBalance[accountId][balance.assetId] = {
938
- amount: this.#convertToHumanReadable(balance.balance, decimals),
939
- };
940
- }
941
- return true;
942
- }
943
- #recordFetchChainFailure({ address, chainId, error, accountId, nativeAssetId, shouldSkipNative, assetsBalance, assetsInfo, failedChains, }) {
944
- log('Failed to fetch balance', { address, chainId, error });
945
- assetsBalance[accountId] ??= {};
946
- if (!shouldSkipNative) {
947
- assetsBalance[accountId][nativeAssetId] = { amount: '0' };
948
- }
949
- const existingNativeMeta = this.#getExistingAssetsMetadata()[nativeAssetId];
950
- if (!this.#hasValidDecimals(existingNativeMeta)) {
951
- const chainStatus = this.#chainStatuses[chainId];
952
- if (chainStatus) {
953
- assetsInfo[nativeAssetId] = {
954
- type: 'native',
955
- symbol: chainStatus.nativeCurrency,
956
- name: chainStatus.nativeCurrency,
957
- decimals: 18,
958
- };
959
- }
960
- }
961
- if (!failedChains.includes(chainId)) {
962
- failedChains.push(chainId);
963
- }
964
- }
965
- #completeFetchResponse({ assetsBalance, assetsInfo, failedChains, chainsToFetch, updateMode, }) {
966
- const response = { updateMode };
967
783
  if (failedChains.length > 0) {
968
784
  log('Fetch PARTIAL - some chains failed', {
969
785
  successChains: chainsToFetch.filter((chain) => !failedChains.includes(chain)),
@@ -977,13 +793,15 @@ export class RpcDataSource extends AbstractDataSource {
977
793
  else {
978
794
  log('Fetch SUCCESS', {
979
795
  chains: chainsToFetch,
980
- accountCount: Object.keys(assetsBalance ?? {}).length,
796
+ accountCount: Object.keys(assetsBalance).length,
981
797
  });
982
798
  }
983
799
  response.assetsBalance = assetsBalance;
800
+ // Include metadata for native tokens if we have any
984
801
  if (Object.keys(assetsInfo).length > 0) {
985
802
  response.assetsInfo = assetsInfo;
986
803
  }
804
+ response.updateMode = 'merge';
987
805
  return response;
988
806
  }
989
807
  /**
@@ -1101,12 +919,6 @@ export class RpcDataSource extends AbstractDataSource {
1101
919
  ...response.errors,
1102
920
  };
1103
921
  }
1104
- if (response.updateMode) {
1105
- context.response = {
1106
- ...context.response,
1107
- updateMode: response.updateMode,
1108
- };
1109
- }
1110
922
  const failedChains = new Set(Object.keys(response.errors ?? {}));
1111
923
  successfullyHandledChains = supportedChains.filter((chainId) => !failedChains.has(chainId));
1112
924
  if (successfullyHandledChains.length > 0) {
@@ -1186,6 +998,8 @@ export class RpcDataSource extends AbstractDataSource {
1186
998
  };
1187
999
  const balanceToken = this.#balanceFetcher.startPolling(balanceInput);
1188
1000
  balancePollingTokens.push(balanceToken);
1001
+ // Token detection is only relevant for "regular" subscriptions —
1002
+ // a customAssetsOnly subscription should never run detection.
1189
1003
  if (request.customAssetsOnly !== true &&
1190
1004
  this.#tokenDetectionEnabled() &&
1191
1005
  this.#useExternalService()) {
@@ -1235,118 +1049,6 @@ export class RpcDataSource extends AbstractDataSource {
1235
1049
  log('Unsubscribed and stopped polling', { subscriptionId });
1236
1050
  }
1237
1051
  }
1238
- /**
1239
- * v5 entries for one account-chain fetch: native plus the ERC-20s the
1240
- * caller scoped via `request.customAssets`. Unchanged from the pre-v6
1241
- * behavior; delete with the rest of the v5 path.
1242
- *
1243
- * @param request - The data request being fetched.
1244
- * @param chainId - Chain being fetched.
1245
- * @param shouldSkipNative - When true, omit the native entry.
1246
- * @returns Entries to hand to the balance fetcher.
1247
- */
1248
- #getAssetsToFetchV5(request, chainId, shouldSkipNative) {
1249
- const assetsToFetch = [];
1250
- if (!shouldSkipNative) {
1251
- // Build a single AssetFetchEntry[] for native + custom ERC-20s
1252
- assetsToFetch.push({
1253
- assetId: this.#getNativeAssetForChain(chainId),
1254
- address: ZERO_ADDRESS,
1255
- });
1256
- }
1257
- if (request.customAssets) {
1258
- const existingMetadata = this.#getExistingAssetsMetadata();
1259
- for (const assetId of request.customAssets) {
1260
- try {
1261
- const parsed = parseCaipAssetType(assetId);
1262
- const assetChainId = `${parsed.chain.namespace}:${parsed.chain.reference}`;
1263
- if (assetChainId === chainId &&
1264
- this.#getAssetType(assetId) === 'erc20') {
1265
- const tokenAddress = parsed.assetReference.toLowerCase();
1266
- const normalizedId = normalizeAssetId(assetId);
1267
- const decimals = existingMetadata[normalizedId]?.decimals;
1268
- assetsToFetch.push({
1269
- assetId,
1270
- address: tokenAddress,
1271
- decimals,
1272
- });
1273
- }
1274
- }
1275
- catch {
1276
- // Skip unparseable asset IDs
1277
- }
1278
- }
1279
- }
1280
- return assetsToFetch;
1281
- }
1282
- /**
1283
- * v6 entries for one account-chain fetch: the shared visible set (native,
1284
- * pins, default tracked) plus already-tracked ERC-20s that are not hidden.
1285
- * Chains with no native token omit native from `visibleAssetIds`. Hide is
1286
- * only applied to `assetsBalance` rows. The request is never scoped via
1287
- * `customAssets` on v6.
1288
- *
1289
- * @param accountId - Account being fetched.
1290
- * @param chainId - Chain being fetched.
1291
- * @returns Entries to hand to the balance fetcher.
1292
- */
1293
- #getAssetsToFetchV6(accountId, chainId) {
1294
- const { assetsInfo = {}, assetsBalance = {} } = this.#getAssetsState();
1295
- const { visibleAssetIds, hiddenAssetIds } = this.#getAssetVisibility([accountId], [chainId]);
1296
- const hidden = new Set(hiddenAssetIds);
1297
- const assetsToFetch = [];
1298
- const seen = new Set();
1299
- // Fetch visible assets
1300
- for (const assetId of visibleAssetIds) {
1301
- try {
1302
- if (this.#getAssetType(assetId) === 'native') {
1303
- assetsToFetch.push({ assetId, address: ZERO_ADDRESS });
1304
- continue;
1305
- }
1306
- const parsed = parseCaipAssetType(assetId);
1307
- const assetChainId = `${parsed.chain.namespace}:${parsed.chain.reference}`;
1308
- if (assetChainId !== chainId ||
1309
- this.#getAssetType(assetId) !== 'erc20' ||
1310
- seen.has(assetId)) {
1311
- continue;
1312
- }
1313
- seen.add(assetId);
1314
- assetsToFetch.push({
1315
- assetId,
1316
- address: parsed.assetReference.toLowerCase(),
1317
- decimals: assetsInfo[assetId]?.decimals,
1318
- });
1319
- }
1320
- catch {
1321
- // Skip unparseable asset IDs
1322
- }
1323
- }
1324
- // Fetch already-tracked ERC-20s that are not hidden
1325
- for (const assetId of Object.keys(assetsBalance[accountId] ?? {})) {
1326
- try {
1327
- const parsed = parseCaipAssetType(assetId);
1328
- const assetChainId = `${parsed.chain.namespace}:${parsed.chain.reference}`;
1329
- const normalizedId = normalizeAssetId(assetId);
1330
- if (hidden.has(normalizedId) ||
1331
- isStakingContractAssetId(assetId) ||
1332
- assetChainId !== chainId ||
1333
- this.#getAssetType(assetId) !== 'erc20' ||
1334
- seen.has(normalizedId)) {
1335
- continue;
1336
- }
1337
- seen.add(normalizedId);
1338
- assetsToFetch.push({
1339
- assetId,
1340
- address: parsed.assetReference.toLowerCase(),
1341
- decimals: assetsInfo[normalizedId]?.decimals,
1342
- });
1343
- }
1344
- catch {
1345
- // Skip unparseable asset IDs
1346
- }
1347
- }
1348
- return assetsToFetch;
1349
- }
1350
1052
  /**
1351
1053
  * Get existing assets metadata from AssetsController state.
1352
1054
  * Used to include metadata for ERC20 tokens when returning balance updates.
@@ -1354,7 +1056,14 @@ export class RpcDataSource extends AbstractDataSource {
1354
1056
  * @returns Record of asset IDs to their metadata.
1355
1057
  */
1356
1058
  #getExistingAssetsMetadata() {
1357
- return this.#getAssetsState().assetsInfo ?? {};
1059
+ try {
1060
+ const state = this.#messenger.call('AssetsController:getState');
1061
+ return state.assetsInfo ?? {};
1062
+ }
1063
+ catch (error) {
1064
+ log('Failed to get existing assets metadata', { error });
1065
+ return {};
1066
+ }
1358
1067
  }
1359
1068
  /**
1360
1069
  * Destroy the data source and clean up resources.