@metamask-previews/assets-controller 16.1.0-preview-c6cdff1dd → 16.1.0-preview-9d2a9bdf5

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.
@@ -2,24 +2,20 @@ import type { AssetsDataSource, Middleware } from '../types.js';
2
2
  export type RpcFallbackMiddlewareOptions = {
3
3
  /** The RPC data source to use as a fallback. */
4
4
  rpcDataSource: AssetsDataSource;
5
+ /**
6
+ * When true, recover `unprocessedCustomAssets` (Accounts API v6). When false,
7
+ * re-read stale tracked assets the v5 API omitted (legacy path).
8
+ */
9
+ isBalanceV6Enabled?: () => boolean;
5
10
  };
6
11
  /**
7
- * RpcFallbackMiddleware re-reads balances from the RPC data source in two
8
- * cases:
12
+ * RpcFallbackMiddleware recovers what upstream sources left outstanding on RPC.
9
13
  *
10
- * 1. Chains present in `response.errors` (network error, unprocessedNetworks,
11
- * timeout, …). Successful RPC results are merged into the response and
12
- * their entries are cleared from `response.errors`.
13
- * 2. EVM assets tracked in state (`state.assetsBalance` or
14
- * `state.customAssets`) that this balance response left empty. The Accounts
15
- * API omits tokens it does not index, and a returned `0` is
16
- * indistinguishable from "not indexed", so the `merge` state update would
17
- * otherwise keep a stale amount forever. They are passed to RPC as
18
- * `customAssets` so the balance fetcher includes them in its multicall.
14
+ * Accounts API v5: chains in `response.errors`, plus EVM assets tracked in
15
+ * state that this response left empty (the API omits tokens it does not index).
19
16
  *
20
- * Place this immediately after `createParallelBalanceMiddleware` in the fast
21
- * pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC
22
- * carries never trigger graduation.
17
+ * Accounts API v6: chains in `response.errors`, plus pins in
18
+ * `response.unprocessedCustomAssets`.
23
19
  */
24
20
  export declare class RpcFallbackMiddleware {
25
21
  #private;
@@ -1 +1 @@
1
- {"version":3,"file":"RpcFallbackMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,gBAAgB,EAKhB,UAAU,EACX,MAAM,aAAa,CAAC;AAOrB,MAAM,MAAM,4BAA4B,GAAG;IACzC,gDAAgD;IAChD,aAAa,EAAE,gBAAgB,CAAC;CACjC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,qBAAqB;;IAChC,QAAQ,CAAC,IAAI,2BAAmB;IAIhC,YAAY,OAAO,EAAE,4BAA4B,EAEhD;IAED,OAAO,IAAI,MAAM,CAEhB;IAED,IAAI,gBAAgB,IAAI,UAAU,CAoGjC;CACF"}
1
+ {"version":3,"file":"RpcFallbackMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,gBAAgB,EAMhB,UAAU,EAEX,MAAM,aAAa,CAAC;AAQrB,MAAM,MAAM,4BAA4B,GAAG;IACzC,gDAAgD;IAChD,aAAa,EAAE,gBAAgB,CAAC;IAChC;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;CACpC,CAAC;AAIF;;;;;;;;GAQG;AACH,qBAAa,qBAAqB;;IAChC,QAAQ,CAAC,IAAI,2BAAmB;IAMhC,YAAY,OAAO,EAAE,4BAA4B,EAIhD;IAED,OAAO,IAAI,MAAM,CAEhB;IAED,IAAI,gBAAgB,IAAI,UAAU,CAOjC;CA8LF"}
@@ -2,124 +2,188 @@ import { KnownCaipNamespace } from '@metamask/utils';
2
2
  import { isStakingContractAssetId } from '../data-sources/evm-rpc-services/index.js';
3
3
  import { projectLogger, createModuleLogger } from '../logger.js';
4
4
  import { forDataTypes } from '../types.js';
5
+ import { normalizeAssetId } from '../utils/index.js';
5
6
  import { mergeDataResponses } from './ParallelMiddleware.js';
6
7
  const CONTROLLER_NAME = 'RpcFallbackMiddleware';
7
8
  const log = createModuleLogger(projectLogger, CONTROLLER_NAME);
9
+ const noopNext = async (ctx) => ctx;
8
10
  /**
9
- * RpcFallbackMiddleware re-reads balances from the RPC data source in two
10
- * cases:
11
+ * RpcFallbackMiddleware recovers what upstream sources left outstanding on RPC.
11
12
  *
12
- * 1. Chains present in `response.errors` (network error, unprocessedNetworks,
13
- * timeout, …). Successful RPC results are merged into the response and
14
- * their entries are cleared from `response.errors`.
15
- * 2. EVM assets tracked in state (`state.assetsBalance` or
16
- * `state.customAssets`) that this balance response left empty. The Accounts
17
- * API omits tokens it does not index, and a returned `0` is
18
- * indistinguishable from "not indexed", so the `merge` state update would
19
- * otherwise keep a stale amount forever. They are passed to RPC as
20
- * `customAssets` so the balance fetcher includes them in its multicall.
13
+ * Accounts API v5: chains in `response.errors`, plus EVM assets tracked in
14
+ * state that this response left empty (the API omits tokens it does not index).
21
15
  *
22
- * Place this immediately after `createParallelBalanceMiddleware` in the fast
23
- * pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC
24
- * carries never trigger graduation.
16
+ * Accounts API v6: chains in `response.errors`, plus pins in
17
+ * `response.unprocessedCustomAssets`.
25
18
  */
26
19
  export class RpcFallbackMiddleware {
27
20
  name = CONTROLLER_NAME;
28
21
  #rpcDataSource;
22
+ #isBalanceV6Enabled;
29
23
  constructor(options) {
30
24
  this.#rpcDataSource = options.rpcDataSource;
25
+ this.#isBalanceV6Enabled =
26
+ options.isBalanceV6Enabled ?? (() => false);
31
27
  }
32
28
  getName() {
33
29
  return this.name;
34
30
  }
35
31
  get assetsMiddleware() {
36
32
  return forDataTypes(['balance'], async (ctx, next) => {
37
- const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
38
- const staleAssets = collectStaleTrackedAssets(ctx);
39
- const chainsToFetch = [
40
- ...new Set([
41
- ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),
42
- // Already restricted to requested chains. Their chain may not be
43
- // errored: the Accounts API can answer for a chain while omitting a
44
- // token it does not index.
45
- ...staleAssets.map((assetId) => assetId.split('/')[0]),
46
- ]),
47
- ];
48
- if (chainsToFetch.length === 0) {
49
- return next(ctx);
33
+ if (this.#isBalanceV6Enabled()) {
34
+ return this.#recoverV6(ctx, next);
50
35
  }
51
- log('Re-reading balances on RPC', {
52
- erroredChains: [...erroredChains],
53
- staleAssets,
54
- chains: chainsToFetch,
55
- });
56
- const filteredRequest = {
57
- ...ctx.request,
58
- chainIds: chainsToFetch,
59
- customAssets: [
60
- ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),
61
- ],
62
- };
63
- const noopNext = async (inner) => inner;
64
- const rpcResult = await this.#rpcDataSource.assetsMiddleware({
65
- ...ctx,
66
- request: filteredRequest,
67
- response: {},
68
- }, noopNext);
69
- // A chain RPC itself failed on contributed nothing trustworthy: its
70
- // balances are failure stubs (native 0) that would overwrite correct
71
- // upstream amounts and, with replaceCoveredChainBalances, wipe the
72
- // chain's token slice from state. Drop them before merging — this also
73
- // keeps failed chains from counting as "recovered" below.
74
- const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
75
- const rpcAssetsBalance = filterOutChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
76
- // RPC errors are kept only for chains that were already errored
77
- // upstream. For chains fetched solely for stale tracked assets the
78
- // upstream response succeeded and stays authoritative — the stale asset
79
- // keeps its previous amount and is retried on the next pass.
80
- const rpcErrors = Object.fromEntries(Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) => erroredChains.has(chainId)));
81
- const merged = mergeDataResponses([
82
- ctx.response,
83
- {
84
- ...rpcResult.response,
85
- assetsBalance: rpcAssetsBalance,
86
- errors: rpcErrors,
87
- },
88
- ]);
89
- // Clear errors only for chains RPC actually recovered a balance for.
90
- // We must inspect the (filtered) RPC balances — NOT merged — because
91
- // merged also contains balances from the upstream sources (AccountsApi /
92
- // Websocket / Staked). If those sources returned partial data for
93
- // a chain that they also flagged as errored (e.g. via
94
- // unprocessedNetworks), and RPC then failed for that same chain,
95
- // looking at merged would incorrectly mark the error as recovered.
96
- if (merged.errors && rpcAssetsBalance) {
97
- const chainsRecoveredByRpc = new Set();
98
- for (const accountBalances of Object.values(rpcAssetsBalance)) {
99
- for (const assetId of Object.keys(accountBalances)) {
100
- chainsRecoveredByRpc.add(assetId.split('/')[0]);
101
- }
36
+ return this.#recoverV5(ctx, next);
37
+ });
38
+ }
39
+ async #recoverV6(ctx, next) {
40
+ const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
41
+ const unprocessedCustomAssets = [
42
+ ...new Set(ctx.response.unprocessedCustomAssets ?? []),
43
+ ];
44
+ if (erroredChains.size === 0 && unprocessedCustomAssets.length === 0) {
45
+ return next(ctx);
46
+ }
47
+ let merged = ctx.response;
48
+ if (erroredChains.size > 0) {
49
+ merged = await this.#recoverErroredChains(ctx, merged, erroredChains);
50
+ }
51
+ const assetsToRecover = unprocessedCustomAssets.filter((assetId) => !erroredChains.has(chainIdOfAsset(assetId)));
52
+ if (assetsToRecover.length > 0) {
53
+ merged = await this.#recoverUnprocessedAssets(ctx, merged, assetsToRecover);
54
+ }
55
+ merged = clearRecoveredAssetIds(merged);
56
+ return next({ ...ctx, response: merged });
57
+ }
58
+ async #recoverV5(ctx, next) {
59
+ const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
60
+ const staleAssets = collectStaleTrackedAssets(ctx);
61
+ const chainsToFetch = [
62
+ ...new Set([
63
+ ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),
64
+ ...staleAssets.map((assetId) => assetId.split('/')[0]),
65
+ ]),
66
+ ];
67
+ if (chainsToFetch.length === 0) {
68
+ return next(ctx);
69
+ }
70
+ log('Re-reading balances on RPC', {
71
+ erroredChains: [...erroredChains],
72
+ staleAssets,
73
+ chains: chainsToFetch,
74
+ });
75
+ const filteredRequest = {
76
+ ...ctx.request,
77
+ chainIds: chainsToFetch,
78
+ customAssets: [
79
+ ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),
80
+ ],
81
+ };
82
+ const rpcResult = await this.#rpcDataSource.assetsMiddleware({
83
+ ...ctx,
84
+ request: filteredRequest,
85
+ response: {},
86
+ }, noopNext);
87
+ const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
88
+ const rpcAssetsBalance = filterOutChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
89
+ const rpcErrors = Object.fromEntries(Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) => erroredChains.has(chainId)));
90
+ const merged = mergeDataResponses([
91
+ ctx.response,
92
+ {
93
+ ...rpcResult.response,
94
+ assetsBalance: rpcAssetsBalance,
95
+ errors: rpcErrors,
96
+ },
97
+ ]);
98
+ if (merged.errors && rpcAssetsBalance) {
99
+ const chainsRecoveredByRpc = new Set();
100
+ for (const accountBalances of Object.values(rpcAssetsBalance)) {
101
+ for (const assetId of Object.keys(accountBalances)) {
102
+ chainsRecoveredByRpc.add(assetId.split('/')[0]);
102
103
  }
103
- for (const chainId of erroredChains) {
104
- if (chainsRecoveredByRpc.has(chainId)) {
105
- delete merged.errors[chainId];
106
- }
104
+ }
105
+ for (const chainId of erroredChains) {
106
+ if (chainsRecoveredByRpc.has(chainId)) {
107
+ delete merged.errors[chainId];
107
108
  }
108
109
  }
109
- return next({ ...ctx, response: merged });
110
+ }
111
+ return next({ ...ctx, response: merged });
112
+ }
113
+ async #recoverErroredChains(ctx, currentResponse, erroredChains) {
114
+ log('Retrying failed chains on RPC', { chains: [...erroredChains] });
115
+ const chainRequest = {
116
+ ...ctx.request,
117
+ chainIds: ctx.request.chainIds.filter((id) => erroredChains.has(id)),
118
+ };
119
+ const rpcResult = await this.#rpcDataSource.assetsMiddleware({ ...ctx, request: chainRequest, response: {} }, noopNext);
120
+ const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
121
+ const rpcAssetsBalance = filterOutChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
122
+ const merged = mergeDataResponses([
123
+ currentResponse,
124
+ {
125
+ ...rpcResult.response,
126
+ assetsBalance: rpcAssetsBalance,
127
+ },
128
+ ]);
129
+ if (merged.errors && rpcAssetsBalance) {
130
+ const chainsRecoveredByRpc = new Set();
131
+ for (const accountBalances of Object.values(rpcAssetsBalance)) {
132
+ for (const assetId of Object.keys(accountBalances)) {
133
+ chainsRecoveredByRpc.add(assetId.split('/')[0]);
134
+ }
135
+ }
136
+ for (const chainId of erroredChains) {
137
+ if (chainsRecoveredByRpc.has(chainId)) {
138
+ delete merged.errors[chainId];
139
+ }
140
+ }
141
+ }
142
+ return merged;
143
+ }
144
+ async #recoverUnprocessedAssets(ctx, currentResponse, assetsToRecover) {
145
+ const assetChains = [
146
+ ...new Set(assetsToRecover.map((assetId) => chainIdOfAsset(assetId))),
147
+ ];
148
+ log('Recovering unprocessed pinned assets on RPC', {
149
+ assetIds: assetsToRecover,
110
150
  });
151
+ const assetRequest = {
152
+ ...ctx.request,
153
+ chainIds: assetChains,
154
+ customAssets: assetsToRecover,
155
+ };
156
+ const rpcResult = await this.#rpcDataSource.assetsMiddleware({ ...ctx, request: assetRequest, response: {} }, noopNext);
157
+ return mergeDataResponses([currentResponse, rpcResult.response]);
111
158
  }
112
159
  }
113
- /**
114
- * Remove all balances that belong to the given chains.
115
- *
116
- * Used to discard results for chains the RPC source itself failed on, whose
117
- * entries are failure stubs (native `0`) rather than real readings.
118
- *
119
- * @param assetsBalance - Balances by account from the RPC response.
120
- * @param chainIds - Chains whose balances should be dropped.
121
- * @returns The filtered balance map, without accounts left empty.
122
- */
160
+ function chainIdOfAsset(assetId) {
161
+ return assetId.split('/')[0];
162
+ }
163
+ function clearRecoveredAssetIds(response) {
164
+ if (!response.unprocessedCustomAssets ||
165
+ response.unprocessedCustomAssets.length === 0) {
166
+ return response;
167
+ }
168
+ const recovered = new Set();
169
+ for (const accountBalances of Object.values(response.assetsBalance ?? {})) {
170
+ for (const assetId of Object.keys(accountBalances)) {
171
+ recovered.add(normalizeAssetId(assetId));
172
+ }
173
+ }
174
+ const stillUnprocessed = response.unprocessedCustomAssets.filter((assetId) => !recovered.has(normalizeAssetId(assetId)));
175
+ if (stillUnprocessed.length === response.unprocessedCustomAssets.length) {
176
+ return response;
177
+ }
178
+ const next = { ...response };
179
+ if (stillUnprocessed.length === 0) {
180
+ delete next.unprocessedCustomAssets;
181
+ }
182
+ else {
183
+ next.unprocessedCustomAssets = stillUnprocessed;
184
+ }
185
+ return next;
186
+ }
123
187
  function filterOutChainBalances(assetsBalance, chainIds) {
124
188
  if (!assetsBalance || chainIds.size === 0) {
125
189
  return assetsBalance;
@@ -133,17 +197,6 @@ function filterOutChainBalances(assetsBalance, chainIds) {
133
197
  }
134
198
  return filtered;
135
199
  }
136
- /**
137
- * EVM assets tracked in state that this balance response left empty and RPC
138
- * should re-read.
139
- *
140
- * Limited to chains both requested and supported by the owning account:
141
- * RpcDataSource fetches per account and skips chains outside its supported
142
- * set, so anything else would be queued and then silently dropped.
143
- *
144
- * @param ctx - Pipeline context.
145
- * @returns Asset IDs to hand to the RPC data source.
146
- */
147
200
  function collectStaleTrackedAssets(ctx) {
148
201
  const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } = ctx.getAssetsState();
149
202
  const staleAssets = new Set();
@@ -157,8 +210,6 @@ function collectStaleTrackedAssets(ctx) {
157
210
  const chainsForAccount = supportedChains.filter((chainId) => ctx.request.chainIds.includes(chainId));
158
211
  for (const assetId of trackedAssetIds) {
159
212
  if (isEvmAssetOnChains(assetId, chainsForAccount) &&
160
- // Staked vault balances belong to StakedBalanceDataSource; an RPC
161
- // ERC-20 read of the share token would clobber them.
162
213
  !isStakingContractAssetId(assetId) &&
163
214
  isBalanceEmpty(ctx.response.assetsBalance?.[accountId], assetId)) {
164
215
  staleAssets.add(assetId);
@@ -167,16 +218,6 @@ function collectStaleTrackedAssets(ctx) {
167
218
  }
168
219
  return [...staleAssets];
169
220
  }
170
- /**
171
- * Whether a balance response carries no positive amount for an asset. A
172
- * returned `0` cannot be distinguished from "not indexed", so both count as
173
- * empty. Asset IDs are matched case-insensitively: state keys ERC-20 assets by
174
- * checksummed address, while some data sources return them lower-cased.
175
- *
176
- * @param balances - Balance map for a single account from the response.
177
- * @param assetId - Asset ID to check.
178
- * @returns True when the response holds no positive amount for the asset.
179
- */
180
221
  function isBalanceEmpty(balances, assetId) {
181
222
  let amount = balances?.[assetId]?.amount;
182
223
  if (amount === undefined && balances) {
@@ -185,13 +226,6 @@ function isBalanceEmpty(balances, assetId) {
185
226
  }
186
227
  return !(Number(amount) > 0);
187
228
  }
188
- /**
189
- * Whether an asset is an EVM asset on one of the given chains.
190
- *
191
- * @param assetId - CAIP-19 asset ID.
192
- * @param chainIds - Chains to match against.
193
- * @returns True for EVM assets whose chain is in `chainIds`.
194
- */
195
229
  function isEvmAssetOnChains(assetId, chainIds) {
196
230
  if (!assetId.startsWith(`${KnownCaipNamespace.Eip155}:`)) {
197
231
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"RpcFallbackMiddleware.js","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.js","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD,OAAO,EAAE,wBAAwB,EAAE,MAAM,2CAA2C,CAAC;AACrF,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAY3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAE7D,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAEhD,MAAM,GAAG,GAAG,kBAAkB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;AAY/D,MAAM,QAAQ,GAAG,KAAK,EAAE,GAAY,EAAoB,EAAE,CAAC,GAAG,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,OAAO,qBAAqB;IACvB,IAAI,GAAG,eAAe,CAAC;IAEvB,cAAc,CAAmB;IAEjC,mBAAmB,CAAgB;IAE5C,YAAY,OAAqC;QAC/C,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;QAC5C,IAAI,CAAC,mBAAmB;YACtB,OAAO,CAAC,kBAAkB,IAAI,CAAC,GAAY,EAAE,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC;IAED,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED,IAAI,gBAAgB;QAClB,OAAO,YAAY,CAAC,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACnD,IAAI,IAAI,CAAC,mBAAmB,EAAE,EAAE,CAAC;gBAC/B,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACpC,CAAC;YACD,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAY,EAAE,IAAkB;QAC/C,MAAM,aAAa,GAAG,IAAI,GAAG,CAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAc,CACpD,CAAC;QACF,MAAM,uBAAuB,GAAG;YAC9B,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,uBAAuB,IAAI,EAAE,CAAC;SACvD,CAAC;QAEF,IAAI,aAAa,CAAC,IAAI,KAAK,CAAC,IAAI,uBAAuB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,IAAI,MAAM,GAAiB,GAAG,CAAC,QAAQ,CAAC;QAExC,IAAI,aAAa,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;QACxE,CAAC;QAED,MAAM,eAAe,GAAG,uBAAuB,CAAC,MAAM,CACpD,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CACzD,CAAC;QACF,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC/B,MAAM,GAAG,MAAM,IAAI,CAAC,yBAAyB,CAC3C,GAAG,EACH,MAAM,EACN,eAAe,CAChB,CAAC;QACJ,CAAC;QAED,MAAM,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAC;QAExC,OAAO,IAAI,CAAC,EAAE,GAAG,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAY,EAAE,IAAkB;QAC/C,MAAM,aAAa,GAAG,IAAI,GAAG,CAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAc,CACpD,CAAC;QACF,MAAM,WAAW,GAAG,yBAAyB,CAAC,GAAG,CAAC,CAAC;QAEnD,MAAM,aAAa,GAAG;YACpB,GAAG,IAAI,GAAG,CAAC;gBACT,GAAG,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;gBAC7D,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAY,CAAC;aAClE,CAAC;SACH,CAAC;QAEF,IAAI,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;QAED,GAAG,CAAC,4BAA4B,EAAE;YAChC,aAAa,EAAE,CAAC,GAAG,aAAa,CAAC;YACjC,WAAW;YACX,MAAM,EAAE,aAAa;SACtB,CAAC,CAAC;QAEH,MAAM,eAAe,GAAG;YACtB,GAAG,GAAG,CAAC,OAAO;YACd,QAAQ,EAAE,aAAa;YACvB,YAAY,EAAE;gBACZ,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC,EAAE,GAAG,WAAW,CAAC,CAAC;aAClE;SACF,CAAC;QAEF,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAC1D;YACE,GAAG,GAAG;YACN,OAAO,EAAE,eAAe;YACxB,QAAQ,EAAE,EAAE;SACb,EACD,QAAQ,CACT,CAAC;QAEF,MAAM,eAAe,GAAG,IAAI,GAAG,CAC7B,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAc,CAC1D,CAAC;QACF,MAAM,gBAAgB,GAAG,sBAAsB,CAC7C,SAAS,CAAC,QAAQ,CAAC,aAAa,EAChC,eAAe,CAChB,CAAC;QAEF,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,CAClC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,CACnE,aAAa,CAAC,GAAG,CAAC,OAAkB,CAAC,CACtC,CACF,CAAC;QAEF,MAAM,MAAM,GAAiB,kBAAkB,CAAC;YAC9C,GAAG,CAAC,QAAQ;YACZ;gBACE,GAAG,SAAS,CAAC,QAAQ;gBACrB,aAAa,EAAE,gBAAgB;gBAC/B,MAAM,EAAE,SAAS;aAClB;SACF,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC;YACtC,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAU,CAAC;YAC/C,KAAK,MAAM,eAAe,IAAI,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBAC9D,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;oBACnD,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClD,CAAC;YACH,CAAC;YACD,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;gBACpC,IAAI,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;oBACtC,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC,EAAE,GAAG,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,qBAAqB,CACzB,GAAY,EACZ,eAA6B,EAC7B,aAA2B;QAE3B,GAAG,CAAC,+BAA+B,EAAE,EAAE,MAAM,EAAE,CAAC,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;QAErE,MAAM,YAAY,GAAgB;YAChC,GAAG,GAAG,CAAC,OAAO;YACd,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;SACrE,CAAC;QACF,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAC1D,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,EAAE,EAC/C,QAAQ,CACT,CAAC;QAEF,MAAM,eAAe,GAAG,IAAI,GAAG,CAC7B,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,IAAI,EAAE,CAAc,CAC1D,CAAC;QACF,MAAM,gBAAgB,GAAG,sBAAsB,CAC7C,SAAS,CAAC,QAAQ,CAAC,aAAa,EAChC,eAAe,CAChB,CAAC;QAEF,MAAM,MAAM,GAAG,kBAAkB,CAAC;YAChC,eAAe;YACf;gBACE,GAAG,SAAS,CAAC,QAAQ;gBACrB,aAAa,EAAE,gBAAgB;aAChC;SACF,CAAC,CAAC;QAEH,IAAI,MAAM,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC;YACtC,MAAM,oBAAoB,GAAG,IAAI,GAAG,EAAU,CAAC;YAC/C,KAAK,MAAM,eAAe,IAAI,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC;gBAC9D,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;oBACnD,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;gBAClD,CAAC;YACH,CAAC;YACD,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;gBACpC,IAAI,oBAAoB,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;oBACtC,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBAChC,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,yBAAyB,CAC7B,GAAY,EACZ,eAA6B,EAC7B,eAAgC;QAEhC,MAAM,WAAW,GAAG;YAClB,GAAG,IAAI,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;SACtE,CAAC;QAEF,GAAG,CAAC,6CAA6C,EAAE;YACjD,QAAQ,EAAE,eAAe;SAC1B,CAAC,CAAC;QAEH,MAAM,YAAY,GAAgB;YAChC,GAAG,GAAG,CAAC,OAAO;YACd,QAAQ,EAAE,WAAW;YACrB,YAAY,EAAE,eAAe;SAC9B,CAAC;QACF,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,gBAAgB,CAC1D,EAAE,GAAG,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,EAAE,EAC/C,QAAQ,CACT,CAAC;QAEF,OAAO,kBAAkB,CAAC,CAAC,eAAe,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IACnE,CAAC;CACF;AAED,SAAS,cAAc,CAAC,OAAsB;IAC5C,OAAO,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAY,CAAC;AAC1C,CAAC;AAED,SAAS,sBAAsB,CAAC,QAAsB;IACpD,IACE,CAAC,QAAQ,CAAC,uBAAuB;QACjC,QAAQ,CAAC,uBAAuB,CAAC,MAAM,KAAK,CAAC,EAC7C,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC3C,KAAK,MAAM,eAAe,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,IAAI,EAAE,CAAC,EAAE,CAAC;QAC1E,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;YACnD,SAAS,CAAC,GAAG,CAAC,gBAAgB,CAAC,OAAwB,CAAC,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IAED,MAAM,gBAAgB,GAAG,QAAQ,CAAC,uBAAuB,CAAC,MAAM,CAC9D,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CACvD,CAAC;IAEF,IAAI,gBAAgB,CAAC,MAAM,KAAK,QAAQ,CAAC,uBAAuB,CAAC,MAAM,EAAE,CAAC;QACxE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;IAC7B,IAAI,gBAAgB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC,uBAAuB,CAAC;IACtC,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,uBAAuB,GAAG,gBAAgB,CAAC;IAClD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,sBAAsB,CAC7B,aAA4C,EAC5C,QAAsB;IAEtB,IAAI,CAAC,aAAa,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC1C,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,MAAM,QAAQ,GAA+C,EAAE,CAAC;IAChE,KAAK,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;QACzE,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAC7B,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,MAAM,CACpC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAY,CAAC,CAC/D,CACqC,CAAC;QACzC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACjC,QAAQ,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,yBAAyB,CAAC,GAAY;IAC7C,MAAM,EAAE,aAAa,EAAE,kBAAkB,EAAE,YAAY,EAAE,iBAAiB,EAAE,GAC1E,GAAG,CAAC,cAAc,EAAE,CAAC;IAEvB,MAAM,WAAW,GAAG,IAAI,GAAG,EAAiB,CAAC;IAE7C,KAAK,MAAM,EAAE,OAAO,EAAE,eAAe,EAAE,IAAI,GAAG,CAAC,OAAO;SACnD,2BAA2B,EAAE,CAAC;QAC/B,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,CAAC;QAC7B,MAAM,eAAe,GAAG,IAAI,GAAG,CAAgB;YAC7C,GAAI,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAqB;YACxE,GAAG,CAAC,iBAAiB,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,gBAAgB,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAC1D,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CACvC,CAAC;QAEF,KAAK,MAAM,OAAO,IAAI,eAAe,EAAE,CAAC;YACtC,IACE,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,CAAC;gBAC7C,CAAC,wBAAwB,CAAC,OAAO,CAAC;gBAClC,cAAc,CAAC,GAAG,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,EAChE,CAAC;gBACD,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,CAAC,GAAG,WAAW,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,cAAc,CACrB,QAAkD,EAClD,OAAsB;IAEtB,IAAI,MAAM,GAAG,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IACzC,IAAI,MAAM,KAAK,SAAS,IAAI,QAAQ,EAAE,CAAC;QACrC,MAAM,iBAAiB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;QAChD,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CACpC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,KAAK,iBAAiB,CACjD,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC;IACD,OAAO,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,kBAAkB,CACzB,OAAsB,EACtB,QAAmB;IAEnB,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,GAAG,kBAAkB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzD,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAY,CAAC,CAAC;AAC7D,CAAC","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"]}
@@ -34,9 +34,14 @@ export type FastFetchSources = {
34
34
  * @param options - Lane options.
35
35
  * @param options.isBasicFunctionality - When false, only the staking balance and
36
36
  * detection run; no network-backed source is used.
37
+ * @param options.includeCustomAssetGraduation - `true` on the Accounts API v5
38
+ * lane. The v6 lane never graduates custom assets: it sends the pins to the
39
+ * endpoint as `includeAssetIds` and keeps the ones it could not resolve as
40
+ * `unprocessedCustomAssets`.
37
41
  * @returns The composed source list, ready for `executeAssetsPipeline`.
38
42
  */
39
43
  export declare function buildFastFetchSources(sources: FastFetchSources, options: {
40
44
  isBasicFunctionality: boolean;
45
+ includeCustomAssetGraduation: boolean;
41
46
  }): AssetsDataSource[];
42
47
  //# sourceMappingURL=buildFastFetchSources.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildFastFetchSources.d.ts","sourceRoot":"","sources":["../../src/pipeline/buildFastFetchSources.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qBAAqB,EAAE,aAAa,CAAC;IACrC,uBAAuB,EAAE,aAAa,CAAC;IACvC,+BAA+B,EAAE,gBAAgB,CAAC;IAClD,qBAAqB,EAAE,gBAAgB,CAAC;IACxC,mBAAmB,EAAE,gBAAgB,CAAC;IACtC,eAAe,EAAE,gBAAgB,CAAC;IAClC,eAAe,EAAE,gBAAgB,CAAC;CACnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE;IAAE,oBAAoB,EAAE,OAAO,CAAA;CAAE,GACzC,gBAAgB,EAAE,CAyBpB"}
1
+ {"version":3,"file":"buildFastFetchSources.d.ts","sourceRoot":"","sources":["../../src/pipeline/buildFastFetchSources.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,qBAAqB,EAAE,aAAa,CAAC;IACrC,uBAAuB,EAAE,aAAa,CAAC;IACvC,+BAA+B,EAAE,gBAAgB,CAAC;IAClD,qBAAqB,EAAE,gBAAgB,CAAC;IACxC,mBAAmB,EAAE,gBAAgB,CAAC;IACtC,eAAe,EAAE,gBAAgB,CAAC;IAClC,eAAe,EAAE,gBAAgB,CAAC;CACnC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,gBAAgB,EACzB,OAAO,EAAE;IACP,oBAAoB,EAAE,OAAO,CAAC;IAC9B,4BAA4B,EAAE,OAAO,CAAC;CACvC,GACA,gBAAgB,EAAE,CA2BpB"}
@@ -17,6 +17,10 @@ import { createParallelBalanceMiddleware, createParallelMiddleware, } from '../m
17
17
  * @param options - Lane options.
18
18
  * @param options.isBasicFunctionality - When false, only the staking balance and
19
19
  * detection run; no network-backed source is used.
20
+ * @param options.includeCustomAssetGraduation - `true` on the Accounts API v5
21
+ * lane. The v6 lane never graduates custom assets: it sends the pins to the
22
+ * endpoint as `includeAssetIds` and keeps the ones it could not resolve as
23
+ * `unprocessedCustomAssets`.
20
24
  * @returns The composed source list, ready for `executeAssetsPipeline`.
21
25
  */
22
26
  export function buildFastFetchSources(sources, options) {
@@ -29,7 +33,9 @@ export function buildFastFetchSources(sources, options) {
29
33
  accountsApiDataSource,
30
34
  stakedBalanceDataSource,
31
35
  ]),
32
- customAssetGraduationMiddleware,
36
+ ...(options.includeCustomAssetGraduation
37
+ ? [customAssetGraduationMiddleware]
38
+ : []),
33
39
  rpcFallbackMiddleware,
34
40
  detectionMiddleware,
35
41
  createParallelMiddleware([tokenDataSource, priceDataSource]),
@@ -1 +1 @@
1
- {"version":3,"file":"buildFastFetchSources.js","sourceRoot":"","sources":["../../src/pipeline/buildFastFetchSources.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,wBAAwB,GACzB,MAAM,sCAAsC,CAAC;AAqB9C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAyB,EACzB,OAA0C;IAE1C,MAAM,EACJ,qBAAqB,EACrB,uBAAuB,EACvB,+BAA+B,EAC/B,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,eAAe,GAChB,GAAG,OAAO,CAAC;IAEZ,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC;QAClC,OAAO,CAAC,uBAAuB,EAAE,mBAAmB,CAAC,CAAC;IACxD,CAAC;IAED,OAAO;QACL,+BAA+B,CAAC;YAC9B,qBAAqB;YACrB,uBAAuB;SACxB,CAAC;QACF,+BAA+B;QAC/B,qBAAqB;QACrB,mBAAmB;QACnB,wBAAwB,CAAC,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;KAC7D,CAAC;AACJ,CAAC","sourcesContent":["import {\n createParallelBalanceMiddleware,\n createParallelMiddleware,\n} from '../middlewares/ParallelMiddleware.js';\nimport type { BalanceSource } from '../middlewares/ParallelMiddleware.js';\nimport type { AssetsDataSource } from '../types.js';\n\n/**\n * The sources the fast fetch lane composes, in the roles the lane assigns them.\n *\n * `AssetsController` constructs all of these unconditionally, so the lane can\n * assume every role is filled. `isBasicFunctionality` decides which of them\n * actually run, not which are supplied.\n */\nexport type FastFetchSources = {\n accountsApiDataSource: BalanceSource;\n stakedBalanceDataSource: BalanceSource;\n customAssetGraduationMiddleware: AssetsDataSource;\n rpcFallbackMiddleware: AssetsDataSource;\n detectionMiddleware: AssetsDataSource;\n tokenDataSource: AssetsDataSource;\n priceDataSource: AssetsDataSource;\n};\n\n/**\n * Compose the fast fetch lane: balances in parallel → custom-asset graduation →\n * RPC fallback → detection → token metadata and prices in parallel.\n *\n * Snap and RPC balance sources are deliberately absent — the controller runs\n * those in a background lane because of their latency.\n *\n * Ordering carries two invariants:\n * - Graduation runs BEFORE the RPC fallback so it only ever sees Accounts\n * API / websocket balances. RPC intentionally carries custom assets and must\n * never trigger graduation.\n * - Detection runs before token and price enrichment, which both read\n * `response.detectedAssets`.\n *\n * @param sources - The sources to place into the lane.\n * @param options - Lane options.\n * @param options.isBasicFunctionality - When false, only the staking balance and\n * detection run; no network-backed source is used.\n * @returns The composed source list, ready for `executeAssetsPipeline`.\n */\nexport function buildFastFetchSources(\n sources: FastFetchSources,\n options: { isBasicFunctionality: boolean },\n): AssetsDataSource[] {\n const {\n accountsApiDataSource,\n stakedBalanceDataSource,\n customAssetGraduationMiddleware,\n rpcFallbackMiddleware,\n detectionMiddleware,\n tokenDataSource,\n priceDataSource,\n } = sources;\n\n if (!options.isBasicFunctionality) {\n return [stakedBalanceDataSource, detectionMiddleware];\n }\n\n return [\n createParallelBalanceMiddleware([\n accountsApiDataSource,\n stakedBalanceDataSource,\n ]),\n customAssetGraduationMiddleware,\n rpcFallbackMiddleware,\n detectionMiddleware,\n createParallelMiddleware([tokenDataSource, priceDataSource]),\n ];\n}\n"]}
1
+ {"version":3,"file":"buildFastFetchSources.js","sourceRoot":"","sources":["../../src/pipeline/buildFastFetchSources.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,+BAA+B,EAC/B,wBAAwB,GACzB,MAAM,sCAAsC,CAAC;AAqB9C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,qBAAqB,CACnC,OAAyB,EACzB,OAGC;IAED,MAAM,EACJ,qBAAqB,EACrB,uBAAuB,EACvB,+BAA+B,EAC/B,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,eAAe,GAChB,GAAG,OAAO,CAAC;IAEZ,IAAI,CAAC,OAAO,CAAC,oBAAoB,EAAE,CAAC;QAClC,OAAO,CAAC,uBAAuB,EAAE,mBAAmB,CAAC,CAAC;IACxD,CAAC;IAED,OAAO;QACL,+BAA+B,CAAC;YAC9B,qBAAqB;YACrB,uBAAuB;SACxB,CAAC;QACF,GAAG,CAAC,OAAO,CAAC,4BAA4B;YACtC,CAAC,CAAC,CAAC,+BAA+B,CAAC;YACnC,CAAC,CAAC,EAAE,CAAC;QACP,qBAAqB;QACrB,mBAAmB;QACnB,wBAAwB,CAAC,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;KAC7D,CAAC;AACJ,CAAC","sourcesContent":["import {\n createParallelBalanceMiddleware,\n createParallelMiddleware,\n} from '../middlewares/ParallelMiddleware.js';\nimport type { BalanceSource } from '../middlewares/ParallelMiddleware.js';\nimport type { AssetsDataSource } from '../types.js';\n\n/**\n * The sources the fast fetch lane composes, in the roles the lane assigns them.\n *\n * `AssetsController` constructs all of these unconditionally, so the lane can\n * assume every role is filled. `isBasicFunctionality` decides which of them\n * actually run, not which are supplied.\n */\nexport type FastFetchSources = {\n accountsApiDataSource: BalanceSource;\n stakedBalanceDataSource: BalanceSource;\n customAssetGraduationMiddleware: AssetsDataSource;\n rpcFallbackMiddleware: AssetsDataSource;\n detectionMiddleware: AssetsDataSource;\n tokenDataSource: AssetsDataSource;\n priceDataSource: AssetsDataSource;\n};\n\n/**\n * Compose the fast fetch lane: balances in parallel → custom-asset graduation →\n * RPC fallback → detection → token metadata and prices in parallel.\n *\n * Snap and RPC balance sources are deliberately absent — the controller runs\n * those in a background lane because of their latency.\n *\n * Ordering carries two invariants:\n * - Graduation runs BEFORE the RPC fallback so it only ever sees Accounts\n * API / websocket balances. RPC intentionally carries custom assets and must\n * never trigger graduation.\n * - Detection runs before token and price enrichment, which both read\n * `response.detectedAssets`.\n *\n * @param sources - The sources to place into the lane.\n * @param options - Lane options.\n * @param options.isBasicFunctionality - When false, only the staking balance and\n * detection run; no network-backed source is used.\n * @param options.includeCustomAssetGraduation - `true` on the Accounts API v5\n * lane. The v6 lane never graduates custom assets: it sends the pins to the\n * endpoint as `includeAssetIds` and keeps the ones it could not resolve as\n * `unprocessedCustomAssets`.\n * @returns The composed source list, ready for `executeAssetsPipeline`.\n */\nexport function buildFastFetchSources(\n sources: FastFetchSources,\n options: {\n isBasicFunctionality: boolean;\n includeCustomAssetGraduation: boolean;\n },\n): AssetsDataSource[] {\n const {\n accountsApiDataSource,\n stakedBalanceDataSource,\n customAssetGraduationMiddleware,\n rpcFallbackMiddleware,\n detectionMiddleware,\n tokenDataSource,\n priceDataSource,\n } = sources;\n\n if (!options.isBasicFunctionality) {\n return [stakedBalanceDataSource, detectionMiddleware];\n }\n\n return [\n createParallelBalanceMiddleware([\n accountsApiDataSource,\n stakedBalanceDataSource,\n ]),\n ...(options.includeCustomAssetGraduation\n ? [customAssetGraduationMiddleware]\n : []),\n rpcFallbackMiddleware,\n detectionMiddleware,\n createParallelMiddleware([tokenDataSource, priceDataSource]),\n ];\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -279,13 +279,18 @@ export type DataRequest = {
279
279
  assetTypes?: AssetType[];
280
280
  /** Which data to fetch */
281
281
  dataTypes: DataType[];
282
- /** Specific CAIP-19 asset IDs */
282
+ /**
283
+ * Optional scoped CAIP-19 asset IDs for this fetch (not the full pin list).
284
+ * Used by `getAssets({ customAssets })`, `addCustomAsset`, and RPC fallback
285
+ * recovery. When omitted, v6 sources read pins from controller state.
286
+ */
283
287
  customAssets?: Caip19AssetId[];
284
288
  /**
285
289
  * When true, the data source should poll only the user's `customAssets`
286
290
  * for the requested chains and skip refreshing the regular tracked
287
291
  * balances. Used by the AssetsController to issue a supplemental RPC
288
- * subscription on chains that another data source is already covering.
292
+ * subscription on chains that another data source is already covering
293
+ * (Accounts API v5 path). Ignored when Accounts API v6 is enabled.
289
294
  */
290
295
  customAssetsOnly?: boolean;
291
296
  /** Force fresh fetch, bypass cache */
@@ -313,8 +318,14 @@ export type DataResponse = {
313
318
  assetsPrice?: Record<Caip19AssetId, AssetPrice>;
314
319
  /** Balance data per account */
315
320
  assetsBalance?: Record<AccountId, Record<Caip19AssetId, AssetBalance>>;
316
- /** Errors encountered, keyed by chain ID */
321
+ /** Errors encountered, keyed by chain ID (chain-axis fallback + telemetry) */
317
322
  errors?: Record<ChainId, string>;
323
+ /**
324
+ * Pinned asset IDs the source could not resolve (asset-axis fallback).
325
+ * Unlike `errors` the chain succeeded — only these assets still need a
326
+ * downstream (RPC) fetch.
327
+ */
328
+ unprocessedCustomAssets?: Caip19AssetId[];
318
329
  /** Detected assets (assets that do not have metadata) */
319
330
  detectedAssets?: Record<AccountId, Caip19AssetId[]>;
320
331
  /**
@@ -326,28 +337,26 @@ export type DataResponse = {
326
337
  * When set with `updateMode: 'merge'`, balances on chains present in
327
338
  * `assetsBalance` replace the prior chain slice (stale tokens on those chains
328
339
  * are dropped). Custom assets on covered chains are preserved. Used for
329
- * `getAssets({ forceUpdate: true })` so unlock/startup reflects the API snapshot
330
- * without switching to `updateMode: 'full'`.
340
+ * Accounts API v5 `getAssets({ forceUpdate: true })`. Accounts API v6 uses
341
+ * `updateMode: 'full'` instead.
331
342
  */
332
343
  replaceCoveredChainBalances?: boolean;
333
344
  };
334
345
  /**
335
346
  * Type of {@link DataResponse.updateMode}: how the controller applies the response to state.
336
347
  *
337
- * - **full**: Response is the full set for the scope. Assets in state but not in the
338
- * response are cleared (except custom assets). Use for initial fetch or full refresh.
339
- * - **merge**: By default only assets present in the response are updated;
340
- * nothing is removed. When {@link DataResponse.replaceCoveredChainBalances}
341
- * is true, balances on chains present in the response replace the prior chain
342
- * slice (stale tokens on those chains are dropped; custom assets preserved).
343
- * Metadata and prices from the response are applied. Use for event-driven updates.
344
- * - **update**: Balance-only overlay — incoming balance amounts are patched in place;
345
- * existing balances, metadata, and prices are never removed or overwritten.
346
- * Missing metadata and prices from the response are seeded so RPC-only chains
347
- * can render on first fetch. Use for force refresh when the API may return a
348
- * partial chain snapshot.
349
- */
350
- export type AssetsUpdateMode = 'full' | 'merge' | 'update';
348
+ * - **full**: Response is the full set for covered chains (those present in
349
+ * `assetsBalance`). Assets in state on those chains but not in the response
350
+ * are cleared, including custom assets (Accounts API v6 returns pins via
351
+ * `includeAssetIds`). The only exception is {@link DataResponse.unprocessedCustomAssets}
352
+ * that RPC fallback also could not recover — those prior balances are kept
353
+ * until a later successful fetch. Use for Accounts API v6 snapshots.
354
+ * - **merge**: Only assets present in the response are updated; nothing is
355
+ * removed, unless {@link DataResponse.replaceCoveredChainBalances} is set
356
+ * (Accounts API v5 force refresh). Metadata and prices from the response
357
+ * are applied. Use for event-driven updates and Accounts API v5.
358
+ */
359
+ export type AssetsUpdateMode = 'full' | 'merge';
351
360
  /**
352
361
  * Callbacks for data sources to report to AssetsController.
353
362
  * Passed to data sources so they report by direct call instead of messenger.