@metamask-previews/assets-controller 14.0.3-preview-e12b5ece6 → 14.0.3-preview-fe4b3be11

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 (80) hide show
  1. package/CHANGELOG.md +5 -10
  2. package/dist/AssetsController.cjs +592 -273
  3. package/dist/AssetsController.cjs.map +1 -1
  4. package/dist/AssetsController.d.cts +7 -10
  5. package/dist/AssetsController.d.cts.map +1 -1
  6. package/dist/AssetsController.d.mts +7 -10
  7. package/dist/AssetsController.d.mts.map +1 -1
  8. package/dist/AssetsController.mjs +592 -273
  9. package/dist/AssetsController.mjs.map +1 -1
  10. package/dist/data-sources/AbstractDataSource.cjs +23 -0
  11. package/dist/data-sources/AbstractDataSource.cjs.map +1 -1
  12. package/dist/data-sources/AbstractDataSource.d.cts +13 -1
  13. package/dist/data-sources/AbstractDataSource.d.cts.map +1 -1
  14. package/dist/data-sources/AbstractDataSource.d.mts +13 -1
  15. package/dist/data-sources/AbstractDataSource.d.mts.map +1 -1
  16. package/dist/data-sources/AbstractDataSource.mjs +23 -0
  17. package/dist/data-sources/AbstractDataSource.mjs.map +1 -1
  18. package/dist/data-sources/AccountActivityDataSource.cjs +2 -0
  19. package/dist/data-sources/AccountActivityDataSource.cjs.map +1 -1
  20. package/dist/data-sources/AccountActivityDataSource.d.cts.map +1 -1
  21. package/dist/data-sources/AccountActivityDataSource.d.mts.map +1 -1
  22. package/dist/data-sources/AccountActivityDataSource.mjs +2 -0
  23. package/dist/data-sources/AccountActivityDataSource.mjs.map +1 -1
  24. package/dist/data-sources/AccountsApiDataSource.cjs +144 -31
  25. package/dist/data-sources/AccountsApiDataSource.cjs.map +1 -1
  26. package/dist/data-sources/AccountsApiDataSource.d.cts +18 -2
  27. package/dist/data-sources/AccountsApiDataSource.d.cts.map +1 -1
  28. package/dist/data-sources/AccountsApiDataSource.d.mts +18 -2
  29. package/dist/data-sources/AccountsApiDataSource.d.mts.map +1 -1
  30. package/dist/data-sources/AccountsApiDataSource.mjs +145 -32
  31. package/dist/data-sources/AccountsApiDataSource.mjs.map +1 -1
  32. package/dist/data-sources/RpcDataSource.cjs +163 -24
  33. package/dist/data-sources/RpcDataSource.cjs.map +1 -1
  34. package/dist/data-sources/RpcDataSource.d.cts +17 -0
  35. package/dist/data-sources/RpcDataSource.d.cts.map +1 -1
  36. package/dist/data-sources/RpcDataSource.d.mts +17 -0
  37. package/dist/data-sources/RpcDataSource.d.mts.map +1 -1
  38. package/dist/data-sources/RpcDataSource.mjs +164 -25
  39. package/dist/data-sources/RpcDataSource.mjs.map +1 -1
  40. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.cjs +21 -12
  41. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.cjs.map +1 -1
  42. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.cts +7 -2
  43. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.cts.map +1 -1
  44. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.mts +7 -2
  45. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.mts.map +1 -1
  46. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.mjs +21 -12
  47. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.mjs.map +1 -1
  48. package/dist/data-sources/evm-rpc-services/types/state.cjs.map +1 -1
  49. package/dist/data-sources/evm-rpc-services/types/state.d.cts +3 -2
  50. package/dist/data-sources/evm-rpc-services/types/state.d.cts.map +1 -1
  51. package/dist/data-sources/evm-rpc-services/types/state.d.mts +3 -2
  52. package/dist/data-sources/evm-rpc-services/types/state.d.mts.map +1 -1
  53. package/dist/data-sources/evm-rpc-services/types/state.mjs.map +1 -1
  54. package/dist/middlewares/ParallelMiddleware.cjs +9 -0
  55. package/dist/middlewares/ParallelMiddleware.cjs.map +1 -1
  56. package/dist/middlewares/ParallelMiddleware.d.cts.map +1 -1
  57. package/dist/middlewares/ParallelMiddleware.d.mts.map +1 -1
  58. package/dist/middlewares/ParallelMiddleware.mjs +9 -0
  59. package/dist/middlewares/ParallelMiddleware.mjs.map +1 -1
  60. package/dist/middlewares/RpcFallbackMiddleware.cjs +158 -128
  61. package/dist/middlewares/RpcFallbackMiddleware.cjs.map +1 -1
  62. package/dist/middlewares/RpcFallbackMiddleware.d.cts +10 -14
  63. package/dist/middlewares/RpcFallbackMiddleware.d.cts.map +1 -1
  64. package/dist/middlewares/RpcFallbackMiddleware.d.mts +10 -14
  65. package/dist/middlewares/RpcFallbackMiddleware.d.mts.map +1 -1
  66. package/dist/middlewares/RpcFallbackMiddleware.mjs +158 -128
  67. package/dist/middlewares/RpcFallbackMiddleware.mjs.map +1 -1
  68. package/dist/selectors/balance.cjs +4 -9
  69. package/dist/selectors/balance.cjs.map +1 -1
  70. package/dist/selectors/balance.d.cts.map +1 -1
  71. package/dist/selectors/balance.d.mts.map +1 -1
  72. package/dist/selectors/balance.mjs +4 -9
  73. package/dist/selectors/balance.mjs.map +1 -1
  74. package/dist/types.cjs.map +1 -1
  75. package/dist/types.d.cts +28 -26
  76. package/dist/types.d.cts.map +1 -1
  77. package/dist/types.d.mts +28 -26
  78. package/dist/types.d.mts.map +1 -1
  79. package/dist/types.mjs.map +1 -1
  80. package/package.json +4 -4
@@ -9,130 +9,190 @@ 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 _RpcFallbackMiddleware_rpcDataSource;
12
+ var _RpcFallbackMiddleware_instances, _RpcFallbackMiddleware_rpcDataSource, _RpcFallbackMiddleware_isBalanceV6Enabled, _RpcFallbackMiddleware_recoverV6, _RpcFallbackMiddleware_recoverV5, _RpcFallbackMiddleware_recoverErroredChains, _RpcFallbackMiddleware_recoverUnprocessedAssets;
13
13
  import { KnownCaipNamespace } from "@metamask/utils";
14
14
  import { isStakingContractAssetId } from "../data-sources/evm-rpc-services/index.mjs";
15
15
  import { projectLogger, createModuleLogger } from "../logger.mjs";
16
16
  import { forDataTypes } from "../types.mjs";
17
+ import { normalizeAssetId } from "../utils/index.mjs";
17
18
  import { mergeDataResponses } from "./ParallelMiddleware.mjs";
18
19
  const CONTROLLER_NAME = 'RpcFallbackMiddleware';
19
20
  const log = createModuleLogger(projectLogger, CONTROLLER_NAME);
21
+ const noopNext = async (ctx) => ctx;
20
22
  /**
21
- * RpcFallbackMiddleware re-reads balances from the RPC data source in two
22
- * cases:
23
+ * RpcFallbackMiddleware recovers what upstream sources left outstanding on RPC.
23
24
  *
24
- * 1. Chains present in `response.errors` (network error, unprocessedNetworks,
25
- * timeout, …). Successful RPC results are merged into the response and
26
- * their entries are cleared from `response.errors`.
27
- * 2. EVM assets tracked in state (`state.assetsBalance` or
28
- * `state.customAssets`) that this balance response left empty. The Accounts
29
- * API omits tokens it does not index, and a returned `0` is
30
- * indistinguishable from "not indexed", so the `merge` state update would
31
- * otherwise keep a stale amount forever. They are passed to RPC as
32
- * `customAssets` so the balance fetcher includes them in its multicall.
25
+ * Accounts API v5: chains in `response.errors`, plus EVM assets tracked in
26
+ * state that this response left empty (the API omits tokens it does not index).
33
27
  *
34
- * Place this immediately after `createParallelBalanceMiddleware` in the fast
35
- * pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC
36
- * carries never trigger graduation.
28
+ * Accounts API v6: chains in `response.errors`, plus pins in
29
+ * `response.unprocessedCustomAssets`.
37
30
  */
38
31
  export class RpcFallbackMiddleware {
39
32
  constructor(options) {
33
+ _RpcFallbackMiddleware_instances.add(this);
40
34
  this.name = CONTROLLER_NAME;
41
35
  _RpcFallbackMiddleware_rpcDataSource.set(this, void 0);
36
+ _RpcFallbackMiddleware_isBalanceV6Enabled.set(this, void 0);
42
37
  __classPrivateFieldSet(this, _RpcFallbackMiddleware_rpcDataSource, options.rpcDataSource, "f");
38
+ __classPrivateFieldSet(this, _RpcFallbackMiddleware_isBalanceV6Enabled, options.isBalanceV6Enabled ?? (() => false), "f");
43
39
  }
44
40
  getName() {
45
41
  return this.name;
46
42
  }
47
43
  get assetsMiddleware() {
48
44
  return forDataTypes(['balance'], async (ctx, next) => {
49
- const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
50
- const staleAssets = collectStaleTrackedAssets(ctx);
51
- const chainsToFetch = [
52
- ...new Set([
53
- ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),
54
- // Already restricted to requested chains. Their chain may not be
55
- // errored: the Accounts API can answer for a chain while omitting a
56
- // token it does not index.
57
- ...staleAssets.map((assetId) => assetId.split('/')[0]),
58
- ]),
59
- ];
60
- if (chainsToFetch.length === 0) {
61
- return next(ctx);
45
+ if (__classPrivateFieldGet(this, _RpcFallbackMiddleware_isBalanceV6Enabled, "f").call(this)) {
46
+ return __classPrivateFieldGet(this, _RpcFallbackMiddleware_instances, "m", _RpcFallbackMiddleware_recoverV6).call(this, ctx, next);
62
47
  }
63
- log('Re-reading balances on RPC', {
64
- erroredChains: [...erroredChains],
65
- staleAssets,
66
- chains: chainsToFetch,
67
- });
68
- const filteredRequest = {
69
- ...ctx.request,
70
- chainIds: chainsToFetch,
71
- customAssets: [
72
- ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),
73
- ],
74
- };
75
- const noopNext = async (inner) => inner;
76
- const rpcResult = await __classPrivateFieldGet(this, _RpcFallbackMiddleware_rpcDataSource, "f").assetsMiddleware({
77
- ...ctx,
78
- request: filteredRequest,
79
- response: {},
80
- }, noopNext);
81
- // A chain RPC itself failed on contributed nothing trustworthy: its
82
- // balances are failure stubs (native 0) that would overwrite correct
83
- // upstream amounts and, with replaceCoveredChainBalances, wipe the
84
- // chain's token slice from state. Drop them before merging — this also
85
- // keeps failed chains from counting as "recovered" below.
86
- const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
87
- const rpcAssetsBalance = filterOutChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
88
- // RPC errors are kept only for chains that were already errored
89
- // upstream. For chains fetched solely for stale tracked assets the
90
- // upstream response succeeded and stays authoritative — the stale asset
91
- // keeps its previous amount and is retried on the next pass.
92
- const rpcErrors = Object.fromEntries(Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) => erroredChains.has(chainId)));
93
- const merged = mergeDataResponses([
94
- ctx.response,
95
- {
96
- ...rpcResult.response,
97
- assetsBalance: rpcAssetsBalance,
98
- errors: rpcErrors,
99
- },
100
- ]);
101
- // Clear errors only for chains RPC actually recovered a balance for.
102
- // We must inspect the (filtered) RPC balances — NOT merged — because
103
- // merged also contains balances from the upstream sources (AccountsApi /
104
- // Websocket / Staked). If those sources returned partial data for
105
- // a chain that they also flagged as errored (e.g. via
106
- // unprocessedNetworks), and RPC then failed for that same chain,
107
- // looking at merged would incorrectly mark the error as recovered.
108
- if (merged.errors && rpcAssetsBalance) {
109
- const chainsRecoveredByRpc = new Set();
110
- for (const accountBalances of Object.values(rpcAssetsBalance)) {
111
- for (const assetId of Object.keys(accountBalances)) {
112
- chainsRecoveredByRpc.add(assetId.split('/')[0]);
113
- }
114
- }
115
- for (const chainId of erroredChains) {
116
- if (chainsRecoveredByRpc.has(chainId)) {
117
- delete merged.errors[chainId];
118
- }
119
- }
120
- }
121
- return next({ ...ctx, response: merged });
48
+ return __classPrivateFieldGet(this, _RpcFallbackMiddleware_instances, "m", _RpcFallbackMiddleware_recoverV5).call(this, ctx, next);
122
49
  });
123
50
  }
124
51
  }
125
- _RpcFallbackMiddleware_rpcDataSource = new WeakMap();
126
- /**
127
- * Remove all balances that belong to the given chains.
128
- *
129
- * Used to discard results for chains the RPC source itself failed on, whose
130
- * entries are failure stubs (native `0`) rather than real readings.
131
- *
132
- * @param assetsBalance - Balances by account from the RPC response.
133
- * @param chainIds - Chains whose balances should be dropped.
134
- * @returns The filtered balance map, without accounts left empty.
135
- */
52
+ _RpcFallbackMiddleware_rpcDataSource = new WeakMap(), _RpcFallbackMiddleware_isBalanceV6Enabled = new WeakMap(), _RpcFallbackMiddleware_instances = new WeakSet(), _RpcFallbackMiddleware_recoverV6 = async function _RpcFallbackMiddleware_recoverV6(ctx, next) {
53
+ const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
54
+ const unprocessedCustomAssets = [
55
+ ...new Set(ctx.response.unprocessedCustomAssets ?? []),
56
+ ];
57
+ if (erroredChains.size === 0 && unprocessedCustomAssets.length === 0) {
58
+ return next(ctx);
59
+ }
60
+ let merged = ctx.response;
61
+ if (erroredChains.size > 0) {
62
+ merged = await __classPrivateFieldGet(this, _RpcFallbackMiddleware_instances, "m", _RpcFallbackMiddleware_recoverErroredChains).call(this, ctx, merged, erroredChains);
63
+ }
64
+ const assetsToRecover = unprocessedCustomAssets.filter((assetId) => !erroredChains.has(chainIdOfAsset(assetId)));
65
+ if (assetsToRecover.length > 0) {
66
+ merged = await __classPrivateFieldGet(this, _RpcFallbackMiddleware_instances, "m", _RpcFallbackMiddleware_recoverUnprocessedAssets).call(this, ctx, merged, assetsToRecover);
67
+ }
68
+ merged = clearRecoveredAssetIds(merged);
69
+ return next({ ...ctx, response: merged });
70
+ }, _RpcFallbackMiddleware_recoverV5 = async function _RpcFallbackMiddleware_recoverV5(ctx, next) {
71
+ const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
72
+ const staleAssets = collectStaleTrackedAssets(ctx);
73
+ const chainsToFetch = [
74
+ ...new Set([
75
+ ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),
76
+ ...staleAssets.map((assetId) => assetId.split('/')[0]),
77
+ ]),
78
+ ];
79
+ if (chainsToFetch.length === 0) {
80
+ return next(ctx);
81
+ }
82
+ log('Re-reading balances on RPC', {
83
+ erroredChains: [...erroredChains],
84
+ staleAssets,
85
+ chains: chainsToFetch,
86
+ });
87
+ const filteredRequest = {
88
+ ...ctx.request,
89
+ chainIds: chainsToFetch,
90
+ customAssets: [
91
+ ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),
92
+ ],
93
+ };
94
+ const rpcResult = await __classPrivateFieldGet(this, _RpcFallbackMiddleware_rpcDataSource, "f").assetsMiddleware({
95
+ ...ctx,
96
+ request: filteredRequest,
97
+ response: {},
98
+ }, noopNext);
99
+ const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
100
+ const rpcAssetsBalance = filterOutChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
101
+ const rpcErrors = Object.fromEntries(Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) => erroredChains.has(chainId)));
102
+ const merged = mergeDataResponses([
103
+ ctx.response,
104
+ {
105
+ ...rpcResult.response,
106
+ assetsBalance: rpcAssetsBalance,
107
+ errors: rpcErrors,
108
+ },
109
+ ]);
110
+ if (merged.errors && rpcAssetsBalance) {
111
+ const chainsRecoveredByRpc = new Set();
112
+ for (const accountBalances of Object.values(rpcAssetsBalance)) {
113
+ for (const assetId of Object.keys(accountBalances)) {
114
+ chainsRecoveredByRpc.add(assetId.split('/')[0]);
115
+ }
116
+ }
117
+ for (const chainId of erroredChains) {
118
+ if (chainsRecoveredByRpc.has(chainId)) {
119
+ delete merged.errors[chainId];
120
+ }
121
+ }
122
+ }
123
+ return next({ ...ctx, response: merged });
124
+ }, _RpcFallbackMiddleware_recoverErroredChains = async function _RpcFallbackMiddleware_recoverErroredChains(ctx, currentResponse, erroredChains) {
125
+ log('Retrying failed chains on RPC', { chains: [...erroredChains] });
126
+ const chainRequest = {
127
+ ...ctx.request,
128
+ chainIds: ctx.request.chainIds.filter((id) => erroredChains.has(id)),
129
+ };
130
+ const rpcResult = await __classPrivateFieldGet(this, _RpcFallbackMiddleware_rpcDataSource, "f").assetsMiddleware({ ...ctx, request: chainRequest, response: {} }, noopNext);
131
+ const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
132
+ const rpcAssetsBalance = filterOutChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
133
+ const merged = mergeDataResponses([
134
+ currentResponse,
135
+ {
136
+ ...rpcResult.response,
137
+ assetsBalance: rpcAssetsBalance,
138
+ },
139
+ ]);
140
+ if (merged.errors && rpcAssetsBalance) {
141
+ const chainsRecoveredByRpc = new Set();
142
+ for (const accountBalances of Object.values(rpcAssetsBalance)) {
143
+ for (const assetId of Object.keys(accountBalances)) {
144
+ chainsRecoveredByRpc.add(assetId.split('/')[0]);
145
+ }
146
+ }
147
+ for (const chainId of erroredChains) {
148
+ if (chainsRecoveredByRpc.has(chainId)) {
149
+ delete merged.errors[chainId];
150
+ }
151
+ }
152
+ }
153
+ return merged;
154
+ }, _RpcFallbackMiddleware_recoverUnprocessedAssets = async function _RpcFallbackMiddleware_recoverUnprocessedAssets(ctx, currentResponse, assetsToRecover) {
155
+ const assetChains = [
156
+ ...new Set(assetsToRecover.map((assetId) => chainIdOfAsset(assetId))),
157
+ ];
158
+ log('Recovering unprocessed pinned assets on RPC', {
159
+ assetIds: assetsToRecover,
160
+ });
161
+ const assetRequest = {
162
+ ...ctx.request,
163
+ chainIds: assetChains,
164
+ customAssets: assetsToRecover,
165
+ };
166
+ const rpcResult = await __classPrivateFieldGet(this, _RpcFallbackMiddleware_rpcDataSource, "f").assetsMiddleware({ ...ctx, request: assetRequest, response: {} }, noopNext);
167
+ return mergeDataResponses([currentResponse, rpcResult.response]);
168
+ };
169
+ function chainIdOfAsset(assetId) {
170
+ return assetId.split('/')[0];
171
+ }
172
+ function clearRecoveredAssetIds(response) {
173
+ if (!response.unprocessedCustomAssets ||
174
+ response.unprocessedCustomAssets.length === 0) {
175
+ return response;
176
+ }
177
+ const recovered = new Set();
178
+ for (const accountBalances of Object.values(response.assetsBalance ?? {})) {
179
+ for (const assetId of Object.keys(accountBalances)) {
180
+ recovered.add(normalizeAssetId(assetId));
181
+ }
182
+ }
183
+ const stillUnprocessed = response.unprocessedCustomAssets.filter((assetId) => !recovered.has(normalizeAssetId(assetId)));
184
+ if (stillUnprocessed.length === response.unprocessedCustomAssets.length) {
185
+ return response;
186
+ }
187
+ const next = { ...response };
188
+ if (stillUnprocessed.length === 0) {
189
+ delete next.unprocessedCustomAssets;
190
+ }
191
+ else {
192
+ next.unprocessedCustomAssets = stillUnprocessed;
193
+ }
194
+ return next;
195
+ }
136
196
  function filterOutChainBalances(assetsBalance, chainIds) {
137
197
  if (!assetsBalance || chainIds.size === 0) {
138
198
  return assetsBalance;
@@ -146,17 +206,6 @@ function filterOutChainBalances(assetsBalance, chainIds) {
146
206
  }
147
207
  return filtered;
148
208
  }
149
- /**
150
- * EVM assets tracked in state that this balance response left empty and RPC
151
- * should re-read.
152
- *
153
- * Limited to chains both requested and supported by the owning account:
154
- * RpcDataSource fetches per account and skips chains outside its supported
155
- * set, so anything else would be queued and then silently dropped.
156
- *
157
- * @param ctx - Pipeline context.
158
- * @returns Asset IDs to hand to the RPC data source.
159
- */
160
209
  function collectStaleTrackedAssets(ctx) {
161
210
  const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } = ctx.getAssetsState();
162
211
  const staleAssets = new Set();
@@ -170,8 +219,6 @@ function collectStaleTrackedAssets(ctx) {
170
219
  const chainsForAccount = supportedChains.filter((chainId) => ctx.request.chainIds.includes(chainId));
171
220
  for (const assetId of trackedAssetIds) {
172
221
  if (isEvmAssetOnChains(assetId, chainsForAccount) &&
173
- // Staked vault balances belong to StakedBalanceDataSource; an RPC
174
- // ERC-20 read of the share token would clobber them.
175
222
  !isStakingContractAssetId(assetId) &&
176
223
  isBalanceEmpty(ctx.response.assetsBalance?.[accountId], assetId)) {
177
224
  staleAssets.add(assetId);
@@ -180,16 +227,6 @@ function collectStaleTrackedAssets(ctx) {
180
227
  }
181
228
  return [...staleAssets];
182
229
  }
183
- /**
184
- * Whether a balance response carries no positive amount for an asset. A
185
- * returned `0` cannot be distinguished from "not indexed", so both count as
186
- * empty. Asset IDs are matched case-insensitively: state keys ERC-20 assets by
187
- * checksummed address, while some data sources return them lower-cased.
188
- *
189
- * @param balances - Balance map for a single account from the response.
190
- * @param assetId - Asset ID to check.
191
- * @returns True when the response holds no positive amount for the asset.
192
- */
193
230
  function isBalanceEmpty(balances, assetId) {
194
231
  let amount = balances?.[assetId]?.amount;
195
232
  if (amount === undefined && balances) {
@@ -198,13 +235,6 @@ function isBalanceEmpty(balances, assetId) {
198
235
  }
199
236
  return !(Number(amount) > 0);
200
237
  }
201
- /**
202
- * Whether an asset is an EVM asset on one of the given chains.
203
- *
204
- * @param assetId - CAIP-19 asset ID.
205
- * @param chainIds - Chains to match against.
206
- * @returns True for EVM assets whose chain is in `chainIds`.
207
- */
208
238
  function isEvmAssetOnChains(assetId, chainIds) {
209
239
  if (!assetId.startsWith(`${KnownCaipNamespace.Eip155}:`)) {
210
240
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"RpcFallbackMiddleware.mjs","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { KnownCaipNamespace } from '@metamask/utils';\n\nimport { isStakingContractAssetId } from '../data-sources/evm-rpc-services/index.js';\nimport { projectLogger, createModuleLogger } from '../logger.js';\nimport { forDataTypes } from '../types.js';\nimport type {\n AssetBalance,\n AssetsDataSource,\n Caip19AssetId,\n ChainId,\n Context,\n DataResponse,\n Middleware,\n} from '../types.js';\nimport { mergeDataResponses } from './ParallelMiddleware.js';\n\nconst CONTROLLER_NAME = 'RpcFallbackMiddleware';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\nexport type RpcFallbackMiddlewareOptions = {\n /** The RPC data source to use as a fallback. */\n rpcDataSource: AssetsDataSource;\n};\n\n/**\n * RpcFallbackMiddleware re-reads balances from the RPC data source in two\n * cases:\n *\n * 1. Chains present in `response.errors` (network error, unprocessedNetworks,\n * timeout, …). Successful RPC results are merged into the response and\n * their entries are cleared from `response.errors`.\n * 2. EVM assets tracked in state (`state.assetsBalance` or\n * `state.customAssets`) that this balance response left empty. The Accounts\n * API omits tokens it does not index, and a returned `0` is\n * indistinguishable from \"not indexed\", so the `merge` state update would\n * otherwise keep a stale amount forever. They are passed to RPC as\n * `customAssets` so the balance fetcher includes them in its multicall.\n *\n * Place this immediately after `createParallelBalanceMiddleware` in the fast\n * pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC\n * carries never trigger graduation.\n */\nexport class RpcFallbackMiddleware {\n readonly name = CONTROLLER_NAME;\n\n readonly #rpcDataSource: AssetsDataSource;\n\n constructor(options: RpcFallbackMiddlewareOptions) {\n this.#rpcDataSource = options.rpcDataSource;\n }\n\n getName(): string {\n return this.name;\n }\n\n get assetsMiddleware(): Middleware {\n return forDataTypes(['balance'], async (ctx, next) => {\n const erroredChains = new Set<ChainId>(\n Object.keys(ctx.response.errors ?? {}) as ChainId[],\n );\n const staleAssets = collectStaleTrackedAssets(ctx);\n\n const chainsToFetch = [\n ...new Set([\n ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),\n // Already restricted to requested chains. Their chain may not be\n // errored: the Accounts API can answer for a chain while omitting a\n // token it does not index.\n ...staleAssets.map((assetId) => assetId.split('/')[0] as ChainId),\n ]),\n ];\n\n if (chainsToFetch.length === 0) {\n return next(ctx);\n }\n\n log('Re-reading balances on RPC', {\n erroredChains: [...erroredChains],\n staleAssets,\n chains: chainsToFetch,\n });\n\n const filteredRequest = {\n ...ctx.request,\n chainIds: chainsToFetch,\n customAssets: [\n ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),\n ],\n };\n\n const noopNext = async (inner: typeof ctx): Promise<typeof ctx> => inner;\n const rpcResult = await this.#rpcDataSource.assetsMiddleware(\n {\n ...ctx,\n request: filteredRequest,\n response: {},\n },\n noopNext,\n );\n\n // A chain RPC itself failed on contributed nothing trustworthy: its\n // balances are failure stubs (native 0) that would overwrite correct\n // upstream amounts and, with replaceCoveredChainBalances, wipe the\n // chain's token slice from state. Drop them before merging — this also\n // keeps failed chains from counting as \"recovered\" below.\n const rpcFailedChains = new Set<ChainId>(\n Object.keys(rpcResult.response.errors ?? {}) as ChainId[],\n );\n const rpcAssetsBalance = filterOutChainBalances(\n rpcResult.response.assetsBalance,\n rpcFailedChains,\n );\n\n // RPC errors are kept only for chains that were already errored\n // upstream. For chains fetched solely for stale tracked assets the\n // upstream response succeeded and stays authoritative — the stale asset\n // keeps its previous amount and is retried on the next pass.\n const rpcErrors = Object.fromEntries(\n Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) =>\n erroredChains.has(chainId as ChainId),\n ),\n );\n\n const merged: DataResponse = mergeDataResponses([\n ctx.response,\n {\n ...rpcResult.response,\n assetsBalance: rpcAssetsBalance,\n errors: rpcErrors,\n },\n ]);\n\n // Clear errors only for chains RPC actually recovered a balance for.\n // We must inspect the (filtered) RPC balances — NOT merged — because\n // merged also contains balances from the upstream sources (AccountsApi /\n // Websocket / Staked). If those sources returned partial data for\n // a chain that they also flagged as errored (e.g. via\n // unprocessedNetworks), and RPC then failed for that same chain,\n // looking at merged would incorrectly mark the error as recovered.\n if (merged.errors && rpcAssetsBalance) {\n const chainsRecoveredByRpc = new Set<string>();\n for (const accountBalances of Object.values(rpcAssetsBalance)) {\n for (const assetId of Object.keys(accountBalances)) {\n chainsRecoveredByRpc.add(assetId.split('/')[0]);\n }\n }\n for (const chainId of erroredChains) {\n if (chainsRecoveredByRpc.has(chainId)) {\n delete merged.errors[chainId];\n }\n }\n }\n\n return next({ ...ctx, response: merged });\n });\n }\n}\n\n/**\n * Remove all balances that belong to the given chains.\n *\n * Used to discard results for chains the RPC source itself failed on, whose\n * entries are failure stubs (native `0`) rather than real readings.\n *\n * @param assetsBalance - Balances by account from the RPC response.\n * @param chainIds - Chains whose balances should be dropped.\n * @returns The filtered balance map, without accounts left empty.\n */\nfunction filterOutChainBalances(\n assetsBalance: DataResponse['assetsBalance'],\n chainIds: Set<ChainId>,\n): DataResponse['assetsBalance'] {\n if (!assetsBalance || chainIds.size === 0) {\n return assetsBalance;\n }\n\n const filtered: NonNullable<DataResponse['assetsBalance']> = {};\n for (const [accountId, accountBalances] of Object.entries(assetsBalance)) {\n const kept = Object.fromEntries(\n Object.entries(accountBalances).filter(\n ([assetId]) => !chainIds.has(assetId.split('/')[0] as ChainId),\n ),\n ) as Record<Caip19AssetId, AssetBalance>;\n if (Object.keys(kept).length > 0) {\n filtered[accountId] = kept;\n }\n }\n return filtered;\n}\n\n/**\n * EVM assets tracked in state that this balance response left empty and RPC\n * should re-read.\n *\n * Limited to chains both requested and supported by the owning account:\n * RpcDataSource fetches per account and skips chains outside its supported\n * set, so anything else would be queued and then silently dropped.\n *\n * @param ctx - Pipeline context.\n * @returns Asset IDs to hand to the RPC data source.\n */\nfunction collectStaleTrackedAssets(ctx: Context): Caip19AssetId[] {\n const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } =\n ctx.getAssetsState();\n\n const staleAssets = new Set<Caip19AssetId>();\n\n for (const { account, supportedChains } of ctx.request\n .accountsWithSupportedChains) {\n const accountId = account.id;\n const trackedAssetIds = new Set<Caip19AssetId>([\n ...(Object.keys(stateAssetsBalance[accountId] ?? {}) as Caip19AssetId[]),\n ...(stateCustomAssets?.[accountId] ?? []),\n ]);\n\n const chainsForAccount = supportedChains.filter((chainId) =>\n ctx.request.chainIds.includes(chainId),\n );\n\n for (const assetId of trackedAssetIds) {\n if (\n isEvmAssetOnChains(assetId, chainsForAccount) &&\n // Staked vault balances belong to StakedBalanceDataSource; an RPC\n // ERC-20 read of the share token would clobber them.\n !isStakingContractAssetId(assetId) &&\n isBalanceEmpty(ctx.response.assetsBalance?.[accountId], assetId)\n ) {\n staleAssets.add(assetId);\n }\n }\n }\n\n return [...staleAssets];\n}\n\n/**\n * Whether a balance response carries no positive amount for an asset. A\n * returned `0` cannot be distinguished from \"not indexed\", so both count as\n * empty. Asset IDs are matched case-insensitively: state keys ERC-20 assets by\n * checksummed address, while some data sources return them lower-cased.\n *\n * @param balances - Balance map for a single account from the response.\n * @param assetId - Asset ID to check.\n * @returns True when the response holds no positive amount for the asset.\n */\nfunction isBalanceEmpty(\n balances: Record<string, AssetBalance> | undefined,\n assetId: Caip19AssetId,\n): boolean {\n let amount = balances?.[assetId]?.amount;\n if (amount === undefined && balances) {\n const lowerCasedAssetId = assetId.toLowerCase();\n amount = Object.entries(balances).find(\n ([id]) => id.toLowerCase() === lowerCasedAssetId,\n )?.[1]?.amount;\n }\n return !(Number(amount) > 0);\n}\n\n/**\n * Whether an asset is an EVM asset on one of the given chains.\n *\n * @param assetId - CAIP-19 asset ID.\n * @param chainIds - Chains to match against.\n * @returns True for EVM assets whose chain is in `chainIds`.\n */\nfunction isEvmAssetOnChains(\n assetId: Caip19AssetId,\n chainIds: ChainId[],\n): boolean {\n if (!assetId.startsWith(`${KnownCaipNamespace.Eip155}:`)) {\n return false;\n }\n return chainIds.includes(assetId.split('/')[0] as ChainId);\n}\n"]}
1
+ {"version":3,"file":"RpcFallbackMiddleware.mjs","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { KnownCaipNamespace } from '@metamask/utils';\n\nimport { isStakingContractAssetId } from '../data-sources/evm-rpc-services/index.js';\nimport { projectLogger, createModuleLogger } from '../logger.js';\nimport { forDataTypes } from '../types.js';\nimport type {\n AssetBalance,\n AssetsDataSource,\n Caip19AssetId,\n ChainId,\n Context,\n DataRequest,\n DataResponse,\n Middleware,\n NextFunction,\n} from '../types.js';\nimport { normalizeAssetId } from '../utils/index.js';\nimport { mergeDataResponses } from './ParallelMiddleware.js';\n\nconst CONTROLLER_NAME = 'RpcFallbackMiddleware';\n\nconst log = createModuleLogger(projectLogger, CONTROLLER_NAME);\n\nexport type RpcFallbackMiddlewareOptions = {\n /** The RPC data source to use as a fallback. */\n rpcDataSource: AssetsDataSource;\n /**\n * When true, recover `unprocessedCustomAssets` (Accounts API v6). When false,\n * re-read stale tracked assets the v5 API omitted (legacy path).\n */\n isBalanceV6Enabled?: () => boolean;\n};\n\nconst noopNext = async (ctx: Context): Promise<Context> => ctx;\n\n/**\n * RpcFallbackMiddleware recovers what upstream sources left outstanding on RPC.\n *\n * Accounts API v5: chains in `response.errors`, plus EVM assets tracked in\n * state that this response left empty (the API omits tokens it does not index).\n *\n * Accounts API v6: chains in `response.errors`, plus pins in\n * `response.unprocessedCustomAssets`.\n */\nexport class RpcFallbackMiddleware {\n readonly name = CONTROLLER_NAME;\n\n readonly #rpcDataSource: AssetsDataSource;\n\n readonly #isBalanceV6Enabled: () => boolean;\n\n constructor(options: RpcFallbackMiddlewareOptions) {\n this.#rpcDataSource = options.rpcDataSource;\n this.#isBalanceV6Enabled =\n options.isBalanceV6Enabled ?? ((): boolean => false);\n }\n\n getName(): string {\n return this.name;\n }\n\n get assetsMiddleware(): Middleware {\n return forDataTypes(['balance'], async (ctx, next) => {\n if (this.#isBalanceV6Enabled()) {\n return this.#recoverV6(ctx, next);\n }\n return this.#recoverV5(ctx, next);\n });\n }\n\n async #recoverV6(ctx: Context, next: NextFunction): Promise<Context> {\n const erroredChains = new Set<ChainId>(\n Object.keys(ctx.response.errors ?? {}) as ChainId[],\n );\n const unprocessedCustomAssets = [\n ...new Set(ctx.response.unprocessedCustomAssets ?? []),\n ];\n\n if (erroredChains.size === 0 && unprocessedCustomAssets.length === 0) {\n return next(ctx);\n }\n\n let merged: DataResponse = ctx.response;\n\n if (erroredChains.size > 0) {\n merged = await this.#recoverErroredChains(ctx, merged, erroredChains);\n }\n\n const assetsToRecover = unprocessedCustomAssets.filter(\n (assetId) => !erroredChains.has(chainIdOfAsset(assetId)),\n );\n if (assetsToRecover.length > 0) {\n merged = await this.#recoverUnprocessedAssets(\n ctx,\n merged,\n assetsToRecover,\n );\n }\n\n merged = clearRecoveredAssetIds(merged);\n\n return next({ ...ctx, response: merged });\n }\n\n async #recoverV5(ctx: Context, next: NextFunction): Promise<Context> {\n const erroredChains = new Set<ChainId>(\n Object.keys(ctx.response.errors ?? {}) as ChainId[],\n );\n const staleAssets = collectStaleTrackedAssets(ctx);\n\n const chainsToFetch = [\n ...new Set([\n ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),\n ...staleAssets.map((assetId) => assetId.split('/')[0] as ChainId),\n ]),\n ];\n\n if (chainsToFetch.length === 0) {\n return next(ctx);\n }\n\n log('Re-reading balances on RPC', {\n erroredChains: [...erroredChains],\n staleAssets,\n chains: chainsToFetch,\n });\n\n const filteredRequest = {\n ...ctx.request,\n chainIds: chainsToFetch,\n customAssets: [\n ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),\n ],\n };\n\n const rpcResult = await this.#rpcDataSource.assetsMiddleware(\n {\n ...ctx,\n request: filteredRequest,\n response: {},\n },\n noopNext,\n );\n\n const rpcFailedChains = new Set<ChainId>(\n Object.keys(rpcResult.response.errors ?? {}) as ChainId[],\n );\n const rpcAssetsBalance = filterOutChainBalances(\n rpcResult.response.assetsBalance,\n rpcFailedChains,\n );\n\n const rpcErrors = Object.fromEntries(\n Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) =>\n erroredChains.has(chainId as ChainId),\n ),\n );\n\n const merged: DataResponse = mergeDataResponses([\n ctx.response,\n {\n ...rpcResult.response,\n assetsBalance: rpcAssetsBalance,\n errors: rpcErrors,\n },\n ]);\n\n if (merged.errors && rpcAssetsBalance) {\n const chainsRecoveredByRpc = new Set<string>();\n for (const accountBalances of Object.values(rpcAssetsBalance)) {\n for (const assetId of Object.keys(accountBalances)) {\n chainsRecoveredByRpc.add(assetId.split('/')[0]);\n }\n }\n for (const chainId of erroredChains) {\n if (chainsRecoveredByRpc.has(chainId)) {\n delete merged.errors[chainId];\n }\n }\n }\n\n return next({ ...ctx, response: merged });\n }\n\n async #recoverErroredChains(\n ctx: Context,\n currentResponse: DataResponse,\n erroredChains: Set<ChainId>,\n ): Promise<DataResponse> {\n log('Retrying failed chains on RPC', { chains: [...erroredChains] });\n\n const chainRequest: DataRequest = {\n ...ctx.request,\n chainIds: ctx.request.chainIds.filter((id) => erroredChains.has(id)),\n };\n const rpcResult = await this.#rpcDataSource.assetsMiddleware(\n { ...ctx, request: chainRequest, response: {} },\n noopNext,\n );\n\n const rpcFailedChains = new Set<ChainId>(\n Object.keys(rpcResult.response.errors ?? {}) as ChainId[],\n );\n const rpcAssetsBalance = filterOutChainBalances(\n rpcResult.response.assetsBalance,\n rpcFailedChains,\n );\n\n const merged = mergeDataResponses([\n currentResponse,\n {\n ...rpcResult.response,\n assetsBalance: rpcAssetsBalance,\n },\n ]);\n\n if (merged.errors && rpcAssetsBalance) {\n const chainsRecoveredByRpc = new Set<string>();\n for (const accountBalances of Object.values(rpcAssetsBalance)) {\n for (const assetId of Object.keys(accountBalances)) {\n chainsRecoveredByRpc.add(assetId.split('/')[0]);\n }\n }\n for (const chainId of erroredChains) {\n if (chainsRecoveredByRpc.has(chainId)) {\n delete merged.errors[chainId];\n }\n }\n }\n\n return merged;\n }\n\n async #recoverUnprocessedAssets(\n ctx: Context,\n currentResponse: DataResponse,\n assetsToRecover: Caip19AssetId[],\n ): Promise<DataResponse> {\n const assetChains = [\n ...new Set(assetsToRecover.map((assetId) => chainIdOfAsset(assetId))),\n ];\n\n log('Recovering unprocessed pinned assets on RPC', {\n assetIds: assetsToRecover,\n });\n\n const assetRequest: DataRequest = {\n ...ctx.request,\n chainIds: assetChains,\n customAssets: assetsToRecover,\n };\n const rpcResult = await this.#rpcDataSource.assetsMiddleware(\n { ...ctx, request: assetRequest, response: {} },\n noopNext,\n );\n\n return mergeDataResponses([currentResponse, rpcResult.response]);\n }\n}\n\nfunction chainIdOfAsset(assetId: Caip19AssetId): ChainId {\n return assetId.split('/')[0] as ChainId;\n}\n\nfunction clearRecoveredAssetIds(response: DataResponse): DataResponse {\n if (\n !response.unprocessedCustomAssets ||\n response.unprocessedCustomAssets.length === 0\n ) {\n return response;\n }\n\n const recovered = new Set<Caip19AssetId>();\n for (const accountBalances of Object.values(response.assetsBalance ?? {})) {\n for (const assetId of Object.keys(accountBalances)) {\n recovered.add(normalizeAssetId(assetId as Caip19AssetId));\n }\n }\n\n const stillUnprocessed = response.unprocessedCustomAssets.filter(\n (assetId) => !recovered.has(normalizeAssetId(assetId)),\n );\n\n if (stillUnprocessed.length === response.unprocessedCustomAssets.length) {\n return response;\n }\n\n const next = { ...response };\n if (stillUnprocessed.length === 0) {\n delete next.unprocessedCustomAssets;\n } else {\n next.unprocessedCustomAssets = stillUnprocessed;\n }\n return next;\n}\n\nfunction filterOutChainBalances(\n assetsBalance: DataResponse['assetsBalance'],\n chainIds: Set<ChainId>,\n): DataResponse['assetsBalance'] {\n if (!assetsBalance || chainIds.size === 0) {\n return assetsBalance;\n }\n\n const filtered: NonNullable<DataResponse['assetsBalance']> = {};\n for (const [accountId, accountBalances] of Object.entries(assetsBalance)) {\n const kept = Object.fromEntries(\n Object.entries(accountBalances).filter(\n ([assetId]) => !chainIds.has(assetId.split('/')[0] as ChainId),\n ),\n ) as Record<Caip19AssetId, AssetBalance>;\n if (Object.keys(kept).length > 0) {\n filtered[accountId] = kept;\n }\n }\n return filtered;\n}\n\nfunction collectStaleTrackedAssets(ctx: Context): Caip19AssetId[] {\n const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } =\n ctx.getAssetsState();\n\n const staleAssets = new Set<Caip19AssetId>();\n\n for (const { account, supportedChains } of ctx.request\n .accountsWithSupportedChains) {\n const accountId = account.id;\n const trackedAssetIds = new Set<Caip19AssetId>([\n ...(Object.keys(stateAssetsBalance[accountId] ?? {}) as Caip19AssetId[]),\n ...(stateCustomAssets?.[accountId] ?? []),\n ]);\n\n const chainsForAccount = supportedChains.filter((chainId) =>\n ctx.request.chainIds.includes(chainId),\n );\n\n for (const assetId of trackedAssetIds) {\n if (\n isEvmAssetOnChains(assetId, chainsForAccount) &&\n !isStakingContractAssetId(assetId) &&\n isBalanceEmpty(ctx.response.assetsBalance?.[accountId], assetId)\n ) {\n staleAssets.add(assetId);\n }\n }\n }\n\n return [...staleAssets];\n}\n\nfunction isBalanceEmpty(\n balances: Record<string, AssetBalance> | undefined,\n assetId: Caip19AssetId,\n): boolean {\n let amount = balances?.[assetId]?.amount;\n if (amount === undefined && balances) {\n const lowerCasedAssetId = assetId.toLowerCase();\n amount = Object.entries(balances).find(\n ([id]) => id.toLowerCase() === lowerCasedAssetId,\n )?.[1]?.amount;\n }\n return !(Number(amount) > 0);\n}\n\nfunction isEvmAssetOnChains(\n assetId: Caip19AssetId,\n chainIds: ChainId[],\n): boolean {\n if (!assetId.startsWith(`${KnownCaipNamespace.Eip155}:`)) {\n return false;\n }\n return chainIds.includes(assetId.split('/')[0] as ChainId);\n}\n"]}
@@ -135,18 +135,13 @@ function mergeBalancesIntoMap(args) {
135
135
  if (assetPreferences?.[typedAssetId]?.hidden) {
136
136
  continue;
137
137
  }
138
- // Balances without a matching `assetsInfo` entry are not renderable
139
- // (no symbol/name/decimals), so they are excluded from aggregation.
140
- const meta = metadata[typedAssetId];
141
- if (!meta) {
142
- continue;
143
- }
144
138
  const info = getAssetInfo(assetInfoCache, typedAssetId);
145
139
  if (!isChainEnabledByMap(enabledNetworkMap, info.chainId)) {
146
140
  continue;
147
141
  }
148
142
  const amountStr = getAmountFromBalance(accountBalances[typedAssetId]);
149
143
  const amountBn = toBigNumberOrZero(amountStr);
144
+ const meta = metadata[typedAssetId];
150
145
  if (amountBn.isZero()) {
151
146
  continue; // skip zeros early to reduce map pressure
152
147
  }
@@ -157,9 +152,9 @@ function mergeBalancesIntoMap(args) {
157
152
  }
158
153
  out.set(typedAssetId, {
159
154
  amount: amountBn,
160
- decimals: meta.decimals,
161
- symbol: meta.symbol,
162
- name: meta.name,
155
+ decimals: meta?.decimals,
156
+ symbol: meta?.symbol,
157
+ name: meta?.name,
163
158
  });
164
159
  }
165
160
  }