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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +0 -28
  2. package/dist/AssetsController-method-action-types.d.ts +1 -5
  3. package/dist/AssetsController-method-action-types.d.ts.map +1 -1
  4. package/dist/AssetsController-method-action-types.js.map +1 -1
  5. package/dist/AssetsController.d.ts +40 -8
  6. package/dist/AssetsController.d.ts.map +1 -1
  7. package/dist/AssetsController.js +235 -570
  8. package/dist/AssetsController.js.map +1 -1
  9. package/dist/data-sources/AbstractDataSource.d.ts +6 -1
  10. package/dist/data-sources/AbstractDataSource.d.ts.map +1 -1
  11. package/dist/data-sources/AbstractDataSource.js.map +1 -1
  12. package/dist/data-sources/AccountsApiDataSource.d.ts +3 -17
  13. package/dist/data-sources/AccountsApiDataSource.d.ts.map +1 -1
  14. package/dist/data-sources/AccountsApiDataSource.js +125 -275
  15. package/dist/data-sources/AccountsApiDataSource.js.map +1 -1
  16. package/dist/data-sources/PriceDataSource.d.ts +6 -7
  17. package/dist/data-sources/PriceDataSource.d.ts.map +1 -1
  18. package/dist/data-sources/PriceDataSource.js +20 -15
  19. package/dist/data-sources/PriceDataSource.js.map +1 -1
  20. package/dist/data-sources/RpcDataSource.d.ts +1 -15
  21. package/dist/data-sources/RpcDataSource.d.ts.map +1 -1
  22. package/dist/data-sources/RpcDataSource.js +107 -398
  23. package/dist/data-sources/RpcDataSource.js.map +1 -1
  24. package/dist/data-sources/SnapDataSource.d.ts +0 -5
  25. package/dist/data-sources/SnapDataSource.d.ts.map +1 -1
  26. package/dist/data-sources/SnapDataSource.js +0 -114
  27. package/dist/data-sources/SnapDataSource.js.map +1 -1
  28. package/dist/data-sources/TokenDataSource.d.ts +1 -3
  29. package/dist/data-sources/TokenDataSource.d.ts.map +1 -1
  30. package/dist/data-sources/TokenDataSource.js +2 -4
  31. package/dist/data-sources/TokenDataSource.js.map +1 -1
  32. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.ts +0 -8
  33. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.ts.map +1 -1
  34. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.js +2 -47
  35. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.js.map +1 -1
  36. package/dist/index.d.ts +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/middlewares/CustomAssetGraduationMiddleware.d.ts +1 -3
  40. package/dist/middlewares/CustomAssetGraduationMiddleware.d.ts.map +1 -1
  41. package/dist/middlewares/CustomAssetGraduationMiddleware.js +1 -3
  42. package/dist/middlewares/CustomAssetGraduationMiddleware.js.map +1 -1
  43. package/dist/middlewares/DetectionMiddleware.d.ts +2 -10
  44. package/dist/middlewares/DetectionMiddleware.d.ts.map +1 -1
  45. package/dist/middlewares/DetectionMiddleware.js +23 -5
  46. package/dist/middlewares/DetectionMiddleware.js.map +1 -1
  47. package/dist/middlewares/ParallelMiddleware.d.ts.map +1 -1
  48. package/dist/middlewares/ParallelMiddleware.js +3 -0
  49. package/dist/middlewares/ParallelMiddleware.js.map +1 -1
  50. package/dist/middlewares/RpcFallbackMiddleware.d.ts +15 -16
  51. package/dist/middlewares/RpcFallbackMiddleware.d.ts.map +1 -1
  52. package/dist/middlewares/RpcFallbackMiddleware.js +107 -125
  53. package/dist/middlewares/RpcFallbackMiddleware.js.map +1 -1
  54. package/dist/middlewares/index.d.ts +0 -1
  55. package/dist/middlewares/index.d.ts.map +1 -1
  56. package/dist/middlewares/index.js.map +1 -1
  57. package/dist/migrations/healAssetsInfoMetadata.d.ts +5 -5
  58. package/dist/migrations/healAssetsInfoMetadata.d.ts.map +1 -1
  59. package/dist/migrations/healAssetsInfoMetadata.js.map +1 -1
  60. package/dist/pipeline/buildFastFetchSources.d.ts +0 -5
  61. package/dist/pipeline/buildFastFetchSources.d.ts.map +1 -1
  62. package/dist/pipeline/buildFastFetchSources.js +1 -7
  63. package/dist/pipeline/buildFastFetchSources.js.map +1 -1
  64. package/dist/pipeline/executeAssetsPipeline.d.ts +3 -1
  65. package/dist/pipeline/executeAssetsPipeline.d.ts.map +1 -1
  66. package/dist/pipeline/executeAssetsPipeline.js +2 -1
  67. package/dist/pipeline/executeAssetsPipeline.js.map +1 -1
  68. package/dist/selectors/balance.js +1 -1
  69. package/dist/selectors/balance.js.map +1 -1
  70. package/dist/types.d.ts +23 -29
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js.map +1 -1
  73. package/package.json +1 -1
  74. package/dist/utils/assetVisibility.d.ts +0 -29
  75. package/dist/utils/assetVisibility.d.ts.map +0 -1
  76. package/dist/utils/assetVisibility.js +0 -80
  77. package/dist/utils/assetVisibility.js.map +0 -1
  78. package/dist/utils/filterFailedChainBalances.d.ts +0 -11
  79. package/dist/utils/filterFailedChainBalances.d.ts.map +0 -1
  80. package/dist/utils/filterFailedChainBalances.js +0 -22
  81. package/dist/utils/filterFailedChainBalances.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import type { AssetsControllerState, Middleware } from '../types.js';
1
+ import type { Middleware } from '../types.js';
2
2
  /**
3
3
  * DetectionMiddleware builds the set of assets that downstream sources use for
4
4
  * metadata and price fetching.
@@ -14,20 +14,12 @@ import type { AssetsControllerState, Middleware } from '../types.js';
14
14
  *
15
15
  * Usage:
16
16
  * ```typescript
17
- * const detectionMiddleware = new DetectionMiddleware({
18
- * getAssetsState: () => this.state,
19
- * });
17
+ * const detectionMiddleware = new DetectionMiddleware();
20
18
  * const middleware = detectionMiddleware.assetsMiddleware;
21
19
  * ```
22
20
  */
23
- export type DetectionMiddlewareOptions = {
24
- /** Current AssetsController state. Used to distinguish newly detected assets. */
25
- getAssetsState: () => AssetsControllerState;
26
- };
27
21
  export declare class DetectionMiddleware {
28
- #private;
29
22
  readonly name = "DetectionMiddleware";
30
- constructor(options: DetectionMiddlewareOptions);
31
23
  getName(): string;
32
24
  /**
33
25
  * Get the middleware that builds detectedAssets for metadata and price fetching.
@@ -1 +1 @@
1
- {"version":3,"file":"DetectionMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAEV,qBAAqB,EAErB,UAAU,EACX,MAAM,aAAa,CAAC;AAgBrB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,0BAA0B,GAAG;IACvC,iFAAiF;IACjF,cAAc,EAAE,MAAM,qBAAqB,CAAC;CAC7C,CAAC;AAEF,qBAAa,mBAAmB;;IAC9B,QAAQ,CAAC,IAAI,yBAAmB;IAIhC,YAAY,OAAO,EAAE,0BAA0B,EAE9C;IAED,OAAO,IAAI,MAAM,CAEhB;IAED;;;;;;;;;;;;;OAaG;IACH,IAAI,gBAAgB,IAAI,UAAU,CA+HjC;CACF"}
1
+ {"version":3,"file":"DetectionMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA4B,UAAU,EAAE,MAAM,aAAa,CAAC;AAgBxE;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,IAAI,yBAAmB;IAEhC,OAAO,IAAI,MAAM,CAEhB;IAED;;;;;;;;;;;;;OAaG;IACH,IAAI,gBAAgB,IAAI,UAAU,CA+HjC;CACF"}
@@ -7,12 +7,30 @@ import { normalizeAssetId } from '../utils/index.js';
7
7
  const CONTROLLER_NAME = 'DetectionMiddleware';
8
8
  // Logger for debugging
9
9
  createModuleLogger(projectLogger, CONTROLLER_NAME);
10
+ // ============================================================================
11
+ // DETECTION MIDDLEWARE
12
+ // ============================================================================
13
+ /**
14
+ * DetectionMiddleware builds the set of assets that downstream sources use for
15
+ * metadata and price fetching.
16
+ *
17
+ * An asset is included in `detectedAssets` only when it is genuinely new:
18
+ * - Assets from `response.assetsBalance` are included only if they are absent
19
+ * from BOTH `state.assetsBalance` (never tracked before) AND `state.assetsInfo`
20
+ * (no metadata yet). Assets already present in either collection are considered
21
+ * known and are intentionally excluded — PriceDataSource's own subscription
22
+ * handles periodic refreshes for those.
23
+ * - Each account's custom assets from state are always included because they
24
+ * may have no balance yet and are explicitly managed by the user.
25
+ *
26
+ * Usage:
27
+ * ```typescript
28
+ * const detectionMiddleware = new DetectionMiddleware();
29
+ * const middleware = detectionMiddleware.assetsMiddleware;
30
+ * ```
31
+ */
10
32
  export class DetectionMiddleware {
11
33
  name = CONTROLLER_NAME;
12
- #getAssetsState;
13
- constructor(options) {
14
- this.#getAssetsState = options.getAssetsState;
15
- }
16
34
  getName() {
17
35
  return this.name;
18
36
  }
@@ -34,7 +52,7 @@ export class DetectionMiddleware {
34
52
  return forDataTypes(['balance'], async (ctx, next) => {
35
53
  const { request, response } = ctx;
36
54
  // Get state for custom assets, existing balances, and existing metadata
37
- const state = this.#getAssetsState();
55
+ const state = ctx.getAssetsState();
38
56
  const { customAssets: stateCustomAssets, assetsBalance: stateAssetsBalance, assetsInfo: stateAssetsInfo, assetsPrice: stateAssetsPrice, } = state;
39
57
  const detectedAssets = {};
40
58
  // 1. From balance response: only include assets that are genuinely new —
@@ -1 +1 @@
1
- {"version":3,"file":"DetectionMiddleware.js","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { projectLogger, createModuleLogger } from '../logger.js';\nimport { forDataTypes } from '../types.js';\nimport type {\n AccountId,\n AssetsControllerState,\n Caip19AssetId,\n Middleware,\n} from '../types.js';\nimport { normalizeAssetId } from '../utils/index.js';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'DetectionMiddleware';\n\n// Logger for debugging\ncreateModuleLogger(projectLogger, CONTROLLER_NAME);\n\n// ============================================================================\n// DETECTION MIDDLEWARE\n// ============================================================================\n\n/**\n * DetectionMiddleware builds the set of assets that downstream sources use for\n * metadata and price fetching.\n *\n * An asset is included in `detectedAssets` only when it is genuinely new:\n * - Assets from `response.assetsBalance` are included only if they are absent\n * from BOTH `state.assetsBalance` (never tracked before) AND `state.assetsInfo`\n * (no metadata yet). Assets already present in either collection are considered\n * known and are intentionally excluded — PriceDataSource's own subscription\n * handles periodic refreshes for those.\n * - Each account's custom assets from state are always included because they\n * may have no balance yet and are explicitly managed by the user.\n *\n * Usage:\n * ```typescript\n * const detectionMiddleware = new DetectionMiddleware({\n * getAssetsState: () => this.state,\n * });\n * const middleware = detectionMiddleware.assetsMiddleware;\n * ```\n */\nexport type DetectionMiddlewareOptions = {\n /** Current AssetsController state. Used to distinguish newly detected assets. */\n getAssetsState: () => AssetsControllerState;\n};\n\nexport class DetectionMiddleware {\n readonly name = CONTROLLER_NAME;\n\n readonly #getAssetsState: () => AssetsControllerState;\n\n constructor(options: DetectionMiddlewareOptions) {\n this.#getAssetsState = options.getAssetsState;\n }\n\n getName(): string {\n return this.name;\n }\n\n /**\n * Get the middleware that builds detectedAssets for metadata and price fetching.\n *\n * This middleware:\n * 1. Includes assets from response.assetsBalance that are absent from both\n * state.assetsBalance and state.assetsInfo (brand-new assets only)\n * 2. Always includes each account's custom assets from state\n * 3. Fills response.detectedAssets with the resulting asset IDs per account\n * 4. Queues detected assets that lack a price in state on\n * request.assetsForPriceUpdate so PriceDataSource fetches them in the same\n * pipeline pass (including the background RPC detection path)\n *\n * @returns The middleware function for the assets pipeline.\n */\n get assetsMiddleware(): Middleware {\n return forDataTypes(['balance'], async (ctx, next) => {\n const { request, response } = ctx;\n\n // Get state for custom assets, existing balances, and existing metadata\n const state = this.#getAssetsState();\n const {\n customAssets: stateCustomAssets,\n assetsBalance: stateAssetsBalance,\n assetsInfo: stateAssetsInfo,\n assetsPrice: stateAssetsPrice,\n } = state;\n\n const detectedAssets: Record<AccountId, Caip19AssetId[]> = {};\n\n // 1. From balance response: only include assets that are genuinely new —\n // not already present in state.assetsBalance or state.assetsInfo.\n if (response.assetsBalance) {\n for (const [accountId, accountBalances] of Object.entries(\n response.assetsBalance,\n )) {\n const detected: Caip19AssetId[] = [];\n\n const stateAccountBalances = stateAssetsBalance[accountId] ?? {};\n\n for (const assetId of Object.keys(\n accountBalances as Record<string, unknown>,\n )) {\n const caipAssetId = assetId as Caip19AssetId;\n // Skip if already tracked in state balances or already has metadata\n if (\n stateAccountBalances[caipAssetId] !== undefined ||\n stateAssetsInfo[caipAssetId] !== undefined\n ) {\n continue;\n }\n detected.push(caipAssetId);\n }\n\n // Merge custom assets for this account, applying the same filter:\n // skip if already in state balance or already has metadata.\n const customForAccount = stateCustomAssets?.[accountId] ?? [];\n for (const assetId of customForAccount) {\n if (detected.includes(assetId)) {\n continue;\n }\n if (\n stateAccountBalances[assetId] !== undefined ||\n stateAssetsInfo[assetId] !== undefined\n ) {\n continue;\n }\n detected.push(assetId);\n }\n\n if (detected.length > 0) {\n detectedAssets[accountId] = detected;\n }\n }\n }\n\n // 2. Accounts in request that weren't in balance response: include their\n // custom assets that are not yet in state.\n for (const { account } of request.accountsWithSupportedChains) {\n const accountId = account.id;\n if (detectedAssets[accountId]) {\n continue;\n }\n const stateAccountBalances = stateAssetsBalance[accountId] ?? {};\n const customForAccount = stateCustomAssets?.[accountId] ?? [];\n const newCustomAssets = customForAccount.filter((assetId) => {\n const inBalance = stateAccountBalances[assetId] !== undefined;\n const inInfo = stateAssetsInfo[assetId] !== undefined;\n return !inBalance && !inInfo;\n });\n if (newCustomAssets.length > 0) {\n detectedAssets[accountId] = newCustomAssets;\n }\n }\n\n if (Object.keys(detectedAssets).length > 0) {\n response.detectedAssets = detectedAssets;\n }\n\n // Queue prices for:\n // 1) newly detected assets missing a price, and\n // 2) assets in this balance response that already exist in state but still\n // lack a price (e.g. natives seeded before the first price poll).\n // PriceDataSource filters non-priceable IDs before calling the API.\n const prices = stateAssetsPrice as Record<string, unknown>;\n const missingPriceAssets = new Set<Caip19AssetId>();\n\n const maybeQueue = (assetId: Caip19AssetId): void => {\n const normalizedAssetId = normalizeAssetId(assetId);\n if (\n prices[normalizedAssetId] === undefined &&\n prices[assetId] === undefined\n ) {\n missingPriceAssets.add(normalizedAssetId);\n }\n };\n\n for (const accountAssetIds of Object.values(detectedAssets)) {\n for (const assetId of accountAssetIds) {\n maybeQueue(assetId);\n }\n }\n\n if (response.assetsBalance) {\n for (const accountBalances of Object.values(response.assetsBalance)) {\n for (const assetId of Object.keys(\n accountBalances as Record<string, unknown>,\n )) {\n maybeQueue(assetId as Caip19AssetId);\n }\n }\n }\n\n if (missingPriceAssets.size > 0) {\n request.assetsForPriceUpdate = [\n ...(request.assetsForPriceUpdate ?? []),\n ...missingPriceAssets,\n ];\n }\n\n return next(ctx);\n });\n }\n}\n"]}
1
+ {"version":3,"file":"DetectionMiddleware.js","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { projectLogger, createModuleLogger } from '../logger.js';\nimport { forDataTypes } from '../types.js';\nimport type { AccountId, Caip19AssetId, Middleware } from '../types.js';\nimport { normalizeAssetId } from '../utils/index.js';\n\n// ============================================================================\n// CONSTANTS\n// ============================================================================\n\nconst CONTROLLER_NAME = 'DetectionMiddleware';\n\n// Logger for debugging\ncreateModuleLogger(projectLogger, CONTROLLER_NAME);\n\n// ============================================================================\n// DETECTION MIDDLEWARE\n// ============================================================================\n\n/**\n * DetectionMiddleware builds the set of assets that downstream sources use for\n * metadata and price fetching.\n *\n * An asset is included in `detectedAssets` only when it is genuinely new:\n * - Assets from `response.assetsBalance` are included only if they are absent\n * from BOTH `state.assetsBalance` (never tracked before) AND `state.assetsInfo`\n * (no metadata yet). Assets already present in either collection are considered\n * known and are intentionally excluded — PriceDataSource's own subscription\n * handles periodic refreshes for those.\n * - Each account's custom assets from state are always included because they\n * may have no balance yet and are explicitly managed by the user.\n *\n * Usage:\n * ```typescript\n * const detectionMiddleware = new DetectionMiddleware();\n * const middleware = detectionMiddleware.assetsMiddleware;\n * ```\n */\nexport class DetectionMiddleware {\n readonly name = CONTROLLER_NAME;\n\n getName(): string {\n return this.name;\n }\n\n /**\n * Get the middleware that builds detectedAssets for metadata and price fetching.\n *\n * This middleware:\n * 1. Includes assets from response.assetsBalance that are absent from both\n * state.assetsBalance and state.assetsInfo (brand-new assets only)\n * 2. Always includes each account's custom assets from state\n * 3. Fills response.detectedAssets with the resulting asset IDs per account\n * 4. Queues detected assets that lack a price in state on\n * request.assetsForPriceUpdate so PriceDataSource fetches them in the same\n * pipeline pass (including the background RPC detection path)\n *\n * @returns The middleware function for the assets pipeline.\n */\n get assetsMiddleware(): Middleware {\n return forDataTypes(['balance'], async (ctx, next) => {\n const { request, response } = ctx;\n\n // Get state for custom assets, existing balances, and existing metadata\n const state = ctx.getAssetsState();\n const {\n customAssets: stateCustomAssets,\n assetsBalance: stateAssetsBalance,\n assetsInfo: stateAssetsInfo,\n assetsPrice: stateAssetsPrice,\n } = state;\n\n const detectedAssets: Record<AccountId, Caip19AssetId[]> = {};\n\n // 1. From balance response: only include assets that are genuinely new —\n // not already present in state.assetsBalance or state.assetsInfo.\n if (response.assetsBalance) {\n for (const [accountId, accountBalances] of Object.entries(\n response.assetsBalance,\n )) {\n const detected: Caip19AssetId[] = [];\n\n const stateAccountBalances = stateAssetsBalance[accountId] ?? {};\n\n for (const assetId of Object.keys(\n accountBalances as Record<string, unknown>,\n )) {\n const caipAssetId = assetId as Caip19AssetId;\n // Skip if already tracked in state balances or already has metadata\n if (\n stateAccountBalances[caipAssetId] !== undefined ||\n stateAssetsInfo[caipAssetId] !== undefined\n ) {\n continue;\n }\n detected.push(caipAssetId);\n }\n\n // Merge custom assets for this account, applying the same filter:\n // skip if already in state balance or already has metadata.\n const customForAccount = stateCustomAssets?.[accountId] ?? [];\n for (const assetId of customForAccount) {\n if (detected.includes(assetId)) {\n continue;\n }\n if (\n stateAccountBalances[assetId] !== undefined ||\n stateAssetsInfo[assetId] !== undefined\n ) {\n continue;\n }\n detected.push(assetId);\n }\n\n if (detected.length > 0) {\n detectedAssets[accountId] = detected;\n }\n }\n }\n\n // 2. Accounts in request that weren't in balance response: include their\n // custom assets that are not yet in state.\n for (const { account } of request.accountsWithSupportedChains) {\n const accountId = account.id;\n if (detectedAssets[accountId]) {\n continue;\n }\n const stateAccountBalances = stateAssetsBalance[accountId] ?? {};\n const customForAccount = stateCustomAssets?.[accountId] ?? [];\n const newCustomAssets = customForAccount.filter((assetId) => {\n const inBalance = stateAccountBalances[assetId] !== undefined;\n const inInfo = stateAssetsInfo[assetId] !== undefined;\n return !inBalance && !inInfo;\n });\n if (newCustomAssets.length > 0) {\n detectedAssets[accountId] = newCustomAssets;\n }\n }\n\n if (Object.keys(detectedAssets).length > 0) {\n response.detectedAssets = detectedAssets;\n }\n\n // Queue prices for:\n // 1) newly detected assets missing a price, and\n // 2) assets in this balance response that already exist in state but still\n // lack a price (e.g. natives seeded before the first price poll).\n // PriceDataSource filters non-priceable IDs before calling the API.\n const prices = stateAssetsPrice as Record<string, unknown>;\n const missingPriceAssets = new Set<Caip19AssetId>();\n\n const maybeQueue = (assetId: Caip19AssetId): void => {\n const normalizedAssetId = normalizeAssetId(assetId);\n if (\n prices[normalizedAssetId] === undefined &&\n prices[assetId] === undefined\n ) {\n missingPriceAssets.add(normalizedAssetId);\n }\n };\n\n for (const accountAssetIds of Object.values(detectedAssets)) {\n for (const assetId of accountAssetIds) {\n maybeQueue(assetId);\n }\n }\n\n if (response.assetsBalance) {\n for (const accountBalances of Object.values(response.assetsBalance)) {\n for (const assetId of Object.keys(\n accountBalances as Record<string, unknown>,\n )) {\n maybeQueue(assetId as Caip19AssetId);\n }\n }\n }\n\n if (missingPriceAssets.size > 0) {\n request.assetsForPriceUpdate = [\n ...(request.assetsForPriceUpdate ?? []),\n ...missingPriceAssets,\n ];\n }\n\n return next(ctx);\n });\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ParallelMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/ParallelMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EAGP,YAAY,EACZ,UAAU,EACV,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAMrB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,YAAY,EAAE,GAAG,YAAY,CAsD1E;AAWD,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,IAAI,MAAM,CAAC;IAClB,gHAAgH;IAChH,mBAAmB,IAAI,OAAO,EAAE,CAAC;IACjC,gBAAgB,EAAE,UAAU,CAAC;CAC9B,CAAC;AAwDF;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG;IACzE,OAAO,IAAI,MAAM,CAAC;IAClB,gBAAgB,EAAE,UAAU,CAAC;CAC9B,CA+GA;AAWD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG;IACrE,OAAO,IAAI,MAAM,CAAC;IAClB,gBAAgB,EAAE,UAAU,CAAC;CAC9B,CAoDA"}
1
+ {"version":3,"file":"ParallelMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/ParallelMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,OAAO,EAGP,YAAY,EACZ,UAAU,EACV,gBAAgB,EACjB,MAAM,aAAa,CAAC;AAMrB;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,YAAY,EAAE,GAAG,YAAY,CAsD1E;AAWD,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,IAAI,MAAM,CAAC;IAClB,gHAAgH;IAChH,mBAAmB,IAAI,OAAO,EAAE,CAAC;IACjC,gBAAgB,EAAE,UAAU,CAAC;CAC9B,CAAC;AAwDF;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG;IACzE,OAAO,IAAI,MAAM,CAAC;IAClB,gBAAgB,EAAE,UAAU,CAAC;CAC9B,CAiHA;AAWD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG;IACrE,OAAO,IAAI,MAAM,CAAC;IAClB,gBAAgB,EAAE,UAAU,CAAC;CAC9B,CAqDA"}
@@ -133,6 +133,7 @@ export function createParallelBalanceMiddleware(sources) {
133
133
  const result = await source.assetsMiddleware({
134
134
  request: requests[i],
135
135
  response: {},
136
+ getAssetsState: context.getAssetsState,
136
137
  }, noopNext);
137
138
  const durationMs = Date.now() - start;
138
139
  return { result, sourceName: source.getName(), durationMs };
@@ -160,6 +161,7 @@ export function createParallelBalanceMiddleware(sources) {
160
161
  const result = await source.assetsMiddleware({
161
162
  request: fallbackRequests[i],
162
163
  response: {},
164
+ getAssetsState: context.getAssetsState,
163
165
  }, noopNext);
164
166
  const durationMs = Date.now() - start;
165
167
  return { result, sourceName: source.getName(), durationMs };
@@ -227,6 +229,7 @@ export function createParallelMiddleware(sources) {
227
229
  const result = await source.assetsMiddleware({
228
230
  request: context.request,
229
231
  response: { ...context.response },
232
+ getAssetsState: context.getAssetsState,
230
233
  }, noopNext);
231
234
  const durationMs = Date.now() - start;
232
235
  return { result, sourceName: source.getName(), durationMs };
@@ -1 +1 @@
1
- {"version":3,"file":"ParallelMiddleware.js","sourceRoot":"","sources":["../../src/middlewares/ParallelMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { Semaphore } from 'async-mutex';\n\nimport type {\n ChainId,\n Context,\n DataRequest,\n DataResponse,\n Middleware,\n AssetsDataSource,\n} from '../types.js';\n\n// ============================================================================\n// MERGE HELPER\n// ============================================================================\n\n/**\n * Deep-merge multiple DataResponses into one.\n * Used when running balance data sources in parallel.\n *\n * @param responses - Array of DataResponse from each source.\n * @returns Single merged DataResponse.\n */\nexport function mergeDataResponses(responses: DataResponse[]): DataResponse {\n const merged: DataResponse = {};\n\n for (const response of responses) {\n if (response.assetsBalance) {\n merged.assetsBalance ??= {};\n for (const [accountId, accountBalances] of Object.entries(\n response.assetsBalance,\n )) {\n merged.assetsBalance[accountId] = {\n ...(merged.assetsBalance[accountId] ?? {}),\n ...accountBalances,\n };\n }\n }\n if (response.assetsInfo) {\n merged.assetsInfo = {\n ...(merged.assetsInfo ?? {}),\n ...response.assetsInfo,\n };\n }\n if (response.assetsPrice) {\n merged.assetsPrice = {\n ...(merged.assetsPrice ?? {}),\n ...response.assetsPrice,\n };\n }\n if (response.errors) {\n merged.errors = {\n ...(merged.errors ?? {}),\n ...response.errors,\n };\n }\n if (response.detectedAssets) {\n merged.detectedAssets = {\n ...(merged.detectedAssets ?? {}),\n ...response.detectedAssets,\n };\n }\n if (response.updateMode === 'full') {\n merged.updateMode = 'full';\n } else if (\n response.updateMode === 'merge' &&\n merged.updateMode !== 'full'\n ) {\n merged.updateMode = 'merge';\n }\n if (response.replaceCoveredChainBalances) {\n merged.replaceCoveredChainBalances = true;\n }\n }\n merged.updateMode ??= 'merge';\n\n return merged;\n}\n\n// ============================================================================\n// PARALLEL BALANCE MIDDLEWARE\n// ============================================================================\n\nconst PARALLEL_BALANCE_MIDDLEWARE_NAME = 'ParallelBalanceMiddleware';\n\n/** Max concurrent balance source calls (round 1 and fallback). */\nconst BALANCE_CONCURRENCY = 3;\n\nexport type BalanceSource = {\n getName(): string;\n /** Chains this source can fetch (e.g. from getActiveChainsSync()). Used to partition chains with no overlap. */\n getActiveChainsSync(): ChainId[];\n assetsMiddleware: Middleware;\n};\n\n/**\n * Partition request.chainIds so each chain is assigned to exactly one source\n * (by source order: first source that supports the chain gets it). Ensures no\n * chain overlap across data source calls.\n *\n * @param request - The data request with chainIds to partition.\n * @param sources - Balance sources in priority order (e.g. AccountsAPI, Snap, Rpc).\n * @returns Array of requests, one per source, each with only that source's assigned chainIds.\n */\nfunction partitionChainsBySource(\n request: DataRequest,\n sources: BalanceSource[],\n): DataRequest[] {\n const { chainIds } = request;\n const assigned = new Set<ChainId>();\n\n return sources.map((source) => {\n const supported = new Set(source.getActiveChainsSync());\n const chainsForSource = chainIds.filter(\n (id) => supported.has(id) && !assigned.has(id),\n );\n chainsForSource.forEach((id) => assigned.add(id));\n\n return {\n ...request,\n chainIds: chainsForSource,\n };\n });\n}\n\n/**\n * Collect chain IDs that failed in the first round (present in response.errors).\n * Used to run a fallback round with remaining sources.\n *\n * @param requests - Partitioned requests, one per source (same order as results).\n * @param results - Results from each source; chain IDs in requests[i] that have errors in results[i].response.errors are considered failed.\n * @returns Set of chain IDs that had errors in the first round.\n */\nfunction getFailedChainIds(\n requests: DataRequest[],\n results: { response: DataResponse }[],\n): Set<ChainId> {\n const failed = new Set<ChainId>();\n for (let i = 0; i < results.length; i++) {\n const errors = results[i].response.errors ?? {};\n for (const chainId of requests[i].chainIds) {\n if (errors[chainId]) {\n failed.add(chainId);\n }\n }\n }\n return failed;\n}\n\n/**\n * Middleware that runs multiple balance data source middlewares in parallel,\n * with no chain overlap. Chains that fail (response.errors) are re-partitioned\n * and fetched again in a fallback round so lower-priority sources can try them.\n *\n * @param sources - Array of balance sources in priority order (each with getName(), getActiveChainsSync(), assetsMiddleware).\n * @returns A single middleware that runs all sources in parallel and merges responses.\n */\nexport function createParallelBalanceMiddleware(sources: BalanceSource[]): {\n getName(): string;\n assetsMiddleware: Middleware;\n} {\n return {\n getName(): string {\n return PARALLEL_BALANCE_MIDDLEWARE_NAME;\n },\n\n assetsMiddleware: async (context, next): Promise<Context> => {\n if (sources.length === 0) {\n return next(context);\n }\n\n const noopNext = async (ctx: typeof context): Promise<typeof context> =>\n ctx;\n const semaphore = new Semaphore(BALANCE_CONCURRENCY);\n\n // Round 1: partition chains (no overlap), run with limited concurrency\n const requests = partitionChainsBySource(context.request, sources);\n const round1Timed = await Promise.all(\n sources.map((source, i) =>\n semaphore.runExclusive(async () => {\n const start = Date.now();\n const result = await source.assetsMiddleware(\n {\n request: requests[i],\n response: {},\n },\n noopNext,\n );\n const durationMs = Date.now() - start;\n return { result, sourceName: source.getName(), durationMs };\n }),\n ),\n );\n const results = round1Timed.map((entry) => entry.result);\n\n const durationByDataSource: Record<string, number> = {\n ...context.durationByDataSource,\n };\n for (const { sourceName, durationMs } of round1Timed) {\n const key = `${PARALLEL_BALANCE_MIDDLEWARE_NAME}.${sourceName}`;\n durationByDataSource[key] =\n (durationByDataSource[key] ?? 0) + durationMs;\n }\n\n let mergedResponse = mergeDataResponses(\n results.map((result) => result.response),\n );\n\n // Fallback: chains that failed (in errors) get re-partitioned and tried again\n const failedChainIds = getFailedChainIds(requests, results);\n if (failedChainIds.size > 0) {\n const fallbackRequest: DataRequest = {\n ...context.request,\n chainIds: [...failedChainIds],\n };\n const fallbackRequests = partitionChainsBySource(\n fallbackRequest,\n sources,\n );\n const fallbackTimed = await Promise.all(\n sources.map((source, i) =>\n semaphore.runExclusive(async () => {\n const start = Date.now();\n const result = await source.assetsMiddleware(\n {\n request: fallbackRequests[i],\n response: {},\n },\n noopNext,\n );\n const durationMs = Date.now() - start;\n return { result, sourceName: source.getName(), durationMs };\n }),\n ),\n );\n const fallbackResults = fallbackTimed.map((entry) => entry.result);\n for (const { sourceName, durationMs } of fallbackTimed) {\n const key = `${PARALLEL_BALANCE_MIDDLEWARE_NAME}.${sourceName}`;\n durationByDataSource[key] =\n (durationByDataSource[key] ?? 0) + durationMs;\n }\n const fallbackMerged = mergeDataResponses(\n fallbackResults.map((result) => result.response),\n );\n mergedResponse = mergeDataResponses([mergedResponse, fallbackMerged]);\n // Remove errors for chains we successfully got balance for in fallback\n if (mergedResponse.errors && mergedResponse.assetsBalance) {\n const chainsWithBalance = new Set<ChainId>();\n for (const accountBalances of Object.values(\n mergedResponse.assetsBalance,\n )) {\n for (const assetId of Object.keys(accountBalances)) {\n const chainId = assetId.split('/')[0] as ChainId;\n chainsWithBalance.add(chainId);\n }\n }\n for (const chainId of failedChainIds) {\n if (chainsWithBalance.has(chainId)) {\n delete mergedResponse.errors[chainId];\n }\n }\n }\n }\n\n return next({\n ...context,\n response: mergeDataResponses([context.response, mergedResponse]),\n durationByDataSource,\n });\n },\n };\n}\n\n// ============================================================================\n// PARALLEL TOKEN/PRICE MIDDLEWARE\n// ============================================================================\n\nconst PARALLEL_MIDDLEWARE_NAME = 'ParallelMiddleware';\n\n/** Max concurrent token/price source calls. */\nconst CONCURRENCY = 2;\n\n/**\n * Middleware that runs multiple data source middlewares (e.g. TokenDataSource,\n * PriceDataSource) in parallel with the same request. Responses are merged so\n * that assetsInfo (token metadata) and assetsPrice are combined. Use this to\n * fetch token and price data concurrently instead of sequentially.\n *\n * @param sources - Array of named middleware sources (getName + assetsMiddleware).\n * @returns A single middleware that runs all sources in parallel and merges responses.\n */\nexport function createParallelMiddleware(sources: AssetsDataSource[]): {\n getName(): string;\n assetsMiddleware: Middleware;\n} {\n return {\n getName(): string {\n return PARALLEL_MIDDLEWARE_NAME;\n },\n\n assetsMiddleware: async (context, next): Promise<Context> => {\n if (sources.length === 0) {\n return next(context);\n }\n\n const noopNext = async (ctx: typeof context): Promise<typeof context> =>\n ctx;\n const semaphore = new Semaphore(CONCURRENCY);\n\n const timedResults = await Promise.all(\n sources.map((source) =>\n semaphore.runExclusive(async () => {\n const start = Date.now();\n const result = await source.assetsMiddleware(\n {\n request: context.request,\n response: { ...context.response },\n },\n noopNext,\n );\n const durationMs = Date.now() - start;\n return { result, sourceName: source.getName(), durationMs };\n }),\n ),\n );\n\n const results = timedResults.map((entry) => entry.result);\n const mergedResponse = mergeDataResponses(\n results.map((result) => result.response),\n );\n\n const durationByDataSource: Record<string, number> = {\n ...context.durationByDataSource,\n };\n for (const { sourceName, durationMs } of timedResults) {\n durationByDataSource[`${PARALLEL_MIDDLEWARE_NAME}.${sourceName}`] =\n durationMs;\n }\n\n return next({\n ...context,\n response: mergeDataResponses([context.response, mergedResponse]),\n durationByDataSource,\n });\n },\n };\n}\n"]}
1
+ {"version":3,"file":"ParallelMiddleware.js","sourceRoot":"","sources":["../../src/middlewares/ParallelMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { Semaphore } from 'async-mutex';\n\nimport type {\n ChainId,\n Context,\n DataRequest,\n DataResponse,\n Middleware,\n AssetsDataSource,\n} from '../types.js';\n\n// ============================================================================\n// MERGE HELPER\n// ============================================================================\n\n/**\n * Deep-merge multiple DataResponses into one.\n * Used when running balance data sources in parallel.\n *\n * @param responses - Array of DataResponse from each source.\n * @returns Single merged DataResponse.\n */\nexport function mergeDataResponses(responses: DataResponse[]): DataResponse {\n const merged: DataResponse = {};\n\n for (const response of responses) {\n if (response.assetsBalance) {\n merged.assetsBalance ??= {};\n for (const [accountId, accountBalances] of Object.entries(\n response.assetsBalance,\n )) {\n merged.assetsBalance[accountId] = {\n ...(merged.assetsBalance[accountId] ?? {}),\n ...accountBalances,\n };\n }\n }\n if (response.assetsInfo) {\n merged.assetsInfo = {\n ...(merged.assetsInfo ?? {}),\n ...response.assetsInfo,\n };\n }\n if (response.assetsPrice) {\n merged.assetsPrice = {\n ...(merged.assetsPrice ?? {}),\n ...response.assetsPrice,\n };\n }\n if (response.errors) {\n merged.errors = {\n ...(merged.errors ?? {}),\n ...response.errors,\n };\n }\n if (response.detectedAssets) {\n merged.detectedAssets = {\n ...(merged.detectedAssets ?? {}),\n ...response.detectedAssets,\n };\n }\n if (response.updateMode === 'full') {\n merged.updateMode = 'full';\n } else if (\n response.updateMode === 'merge' &&\n merged.updateMode !== 'full'\n ) {\n merged.updateMode = 'merge';\n }\n if (response.replaceCoveredChainBalances) {\n merged.replaceCoveredChainBalances = true;\n }\n }\n merged.updateMode ??= 'merge';\n\n return merged;\n}\n\n// ============================================================================\n// PARALLEL BALANCE MIDDLEWARE\n// ============================================================================\n\nconst PARALLEL_BALANCE_MIDDLEWARE_NAME = 'ParallelBalanceMiddleware';\n\n/** Max concurrent balance source calls (round 1 and fallback). */\nconst BALANCE_CONCURRENCY = 3;\n\nexport type BalanceSource = {\n getName(): string;\n /** Chains this source can fetch (e.g. from getActiveChainsSync()). Used to partition chains with no overlap. */\n getActiveChainsSync(): ChainId[];\n assetsMiddleware: Middleware;\n};\n\n/**\n * Partition request.chainIds so each chain is assigned to exactly one source\n * (by source order: first source that supports the chain gets it). Ensures no\n * chain overlap across data source calls.\n *\n * @param request - The data request with chainIds to partition.\n * @param sources - Balance sources in priority order (e.g. AccountsAPI, Snap, Rpc).\n * @returns Array of requests, one per source, each with only that source's assigned chainIds.\n */\nfunction partitionChainsBySource(\n request: DataRequest,\n sources: BalanceSource[],\n): DataRequest[] {\n const { chainIds } = request;\n const assigned = new Set<ChainId>();\n\n return sources.map((source) => {\n const supported = new Set(source.getActiveChainsSync());\n const chainsForSource = chainIds.filter(\n (id) => supported.has(id) && !assigned.has(id),\n );\n chainsForSource.forEach((id) => assigned.add(id));\n\n return {\n ...request,\n chainIds: chainsForSource,\n };\n });\n}\n\n/**\n * Collect chain IDs that failed in the first round (present in response.errors).\n * Used to run a fallback round with remaining sources.\n *\n * @param requests - Partitioned requests, one per source (same order as results).\n * @param results - Results from each source; chain IDs in requests[i] that have errors in results[i].response.errors are considered failed.\n * @returns Set of chain IDs that had errors in the first round.\n */\nfunction getFailedChainIds(\n requests: DataRequest[],\n results: { response: DataResponse }[],\n): Set<ChainId> {\n const failed = new Set<ChainId>();\n for (let i = 0; i < results.length; i++) {\n const errors = results[i].response.errors ?? {};\n for (const chainId of requests[i].chainIds) {\n if (errors[chainId]) {\n failed.add(chainId);\n }\n }\n }\n return failed;\n}\n\n/**\n * Middleware that runs multiple balance data source middlewares in parallel,\n * with no chain overlap. Chains that fail (response.errors) are re-partitioned\n * and fetched again in a fallback round so lower-priority sources can try them.\n *\n * @param sources - Array of balance sources in priority order (each with getName(), getActiveChainsSync(), assetsMiddleware).\n * @returns A single middleware that runs all sources in parallel and merges responses.\n */\nexport function createParallelBalanceMiddleware(sources: BalanceSource[]): {\n getName(): string;\n assetsMiddleware: Middleware;\n} {\n return {\n getName(): string {\n return PARALLEL_BALANCE_MIDDLEWARE_NAME;\n },\n\n assetsMiddleware: async (context, next): Promise<Context> => {\n if (sources.length === 0) {\n return next(context);\n }\n\n const noopNext = async (ctx: typeof context): Promise<typeof context> =>\n ctx;\n const semaphore = new Semaphore(BALANCE_CONCURRENCY);\n\n // Round 1: partition chains (no overlap), run with limited concurrency\n const requests = partitionChainsBySource(context.request, sources);\n const round1Timed = await Promise.all(\n sources.map((source, i) =>\n semaphore.runExclusive(async () => {\n const start = Date.now();\n const result = await source.assetsMiddleware(\n {\n request: requests[i],\n response: {},\n getAssetsState: context.getAssetsState,\n },\n noopNext,\n );\n const durationMs = Date.now() - start;\n return { result, sourceName: source.getName(), durationMs };\n }),\n ),\n );\n const results = round1Timed.map((entry) => entry.result);\n\n const durationByDataSource: Record<string, number> = {\n ...context.durationByDataSource,\n };\n for (const { sourceName, durationMs } of round1Timed) {\n const key = `${PARALLEL_BALANCE_MIDDLEWARE_NAME}.${sourceName}`;\n durationByDataSource[key] =\n (durationByDataSource[key] ?? 0) + durationMs;\n }\n\n let mergedResponse = mergeDataResponses(\n results.map((result) => result.response),\n );\n\n // Fallback: chains that failed (in errors) get re-partitioned and tried again\n const failedChainIds = getFailedChainIds(requests, results);\n if (failedChainIds.size > 0) {\n const fallbackRequest: DataRequest = {\n ...context.request,\n chainIds: [...failedChainIds],\n };\n const fallbackRequests = partitionChainsBySource(\n fallbackRequest,\n sources,\n );\n const fallbackTimed = await Promise.all(\n sources.map((source, i) =>\n semaphore.runExclusive(async () => {\n const start = Date.now();\n const result = await source.assetsMiddleware(\n {\n request: fallbackRequests[i],\n response: {},\n getAssetsState: context.getAssetsState,\n },\n noopNext,\n );\n const durationMs = Date.now() - start;\n return { result, sourceName: source.getName(), durationMs };\n }),\n ),\n );\n const fallbackResults = fallbackTimed.map((entry) => entry.result);\n for (const { sourceName, durationMs } of fallbackTimed) {\n const key = `${PARALLEL_BALANCE_MIDDLEWARE_NAME}.${sourceName}`;\n durationByDataSource[key] =\n (durationByDataSource[key] ?? 0) + durationMs;\n }\n const fallbackMerged = mergeDataResponses(\n fallbackResults.map((result) => result.response),\n );\n mergedResponse = mergeDataResponses([mergedResponse, fallbackMerged]);\n // Remove errors for chains we successfully got balance for in fallback\n if (mergedResponse.errors && mergedResponse.assetsBalance) {\n const chainsWithBalance = new Set<ChainId>();\n for (const accountBalances of Object.values(\n mergedResponse.assetsBalance,\n )) {\n for (const assetId of Object.keys(accountBalances)) {\n const chainId = assetId.split('/')[0] as ChainId;\n chainsWithBalance.add(chainId);\n }\n }\n for (const chainId of failedChainIds) {\n if (chainsWithBalance.has(chainId)) {\n delete mergedResponse.errors[chainId];\n }\n }\n }\n }\n\n return next({\n ...context,\n response: mergeDataResponses([context.response, mergedResponse]),\n durationByDataSource,\n });\n },\n };\n}\n\n// ============================================================================\n// PARALLEL TOKEN/PRICE MIDDLEWARE\n// ============================================================================\n\nconst PARALLEL_MIDDLEWARE_NAME = 'ParallelMiddleware';\n\n/** Max concurrent token/price source calls. */\nconst CONCURRENCY = 2;\n\n/**\n * Middleware that runs multiple data source middlewares (e.g. TokenDataSource,\n * PriceDataSource) in parallel with the same request. Responses are merged so\n * that assetsInfo (token metadata) and assetsPrice are combined. Use this to\n * fetch token and price data concurrently instead of sequentially.\n *\n * @param sources - Array of named middleware sources (getName + assetsMiddleware).\n * @returns A single middleware that runs all sources in parallel and merges responses.\n */\nexport function createParallelMiddleware(sources: AssetsDataSource[]): {\n getName(): string;\n assetsMiddleware: Middleware;\n} {\n return {\n getName(): string {\n return PARALLEL_MIDDLEWARE_NAME;\n },\n\n assetsMiddleware: async (context, next): Promise<Context> => {\n if (sources.length === 0) {\n return next(context);\n }\n\n const noopNext = async (ctx: typeof context): Promise<typeof context> =>\n ctx;\n const semaphore = new Semaphore(CONCURRENCY);\n\n const timedResults = await Promise.all(\n sources.map((source) =>\n semaphore.runExclusive(async () => {\n const start = Date.now();\n const result = await source.assetsMiddleware(\n {\n request: context.request,\n response: { ...context.response },\n getAssetsState: context.getAssetsState,\n },\n noopNext,\n );\n const durationMs = Date.now() - start;\n return { result, sourceName: source.getName(), durationMs };\n }),\n ),\n );\n\n const results = timedResults.map((entry) => entry.result);\n const mergedResponse = mergeDataResponses(\n results.map((result) => result.response),\n );\n\n const durationByDataSource: Record<string, number> = {\n ...context.durationByDataSource,\n };\n for (const { sourceName, durationMs } of timedResults) {\n durationByDataSource[`${PARALLEL_MIDDLEWARE_NAME}.${sourceName}`] =\n durationMs;\n }\n\n return next({\n ...context,\n response: mergeDataResponses([context.response, mergedResponse]),\n durationByDataSource,\n });\n },\n };\n}\n"]}
@@ -1,26 +1,25 @@
1
- import type { AssetsControllerState, AssetsDataSource, Middleware } from '../types.js';
1
+ 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 failed and unprocessed chains with a full RPC refetch
7
- * (Accounts API v6). When false, re-read stale tracked assets the v5 API
8
- * omitted (legacy path).
9
- */
10
- isBalanceV6Enabled?: () => boolean;
11
- /** Current AssetsController state. Used to find tracked assets the response left empty. */
12
- getAssetsState: () => AssetsControllerState;
13
5
  };
14
6
  /**
15
- * RpcFallbackMiddleware recovers what upstream sources left outstanding on RPC.
7
+ * RpcFallbackMiddleware re-reads balances from the RPC data source in two
8
+ * cases:
16
9
  *
17
- * Accounts API v5: chains in `response.errors`, plus EVM assets tracked in
18
- * state that this response left empty (the API omits tokens it does not index).
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.
19
19
  *
20
- * Accounts API v6: one RPC retry on the chain axis. Chains in
21
- * `response.errors` are refetched in full (native + existing balances + pins +
22
- * default tracked assets). RPC reads those from state; the request is not
23
- * scoped via `customAssets`.
20
+ * Place this immediately after `createParallelBalanceMiddleware` in the fast
21
+ * pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC
22
+ * carries never trigger graduation.
24
23
  */
25
24
  export declare class RpcFallbackMiddleware {
26
25
  #private;
@@ -1 +1 @@
1
- {"version":3,"file":"RpcFallbackMiddleware.d.ts","sourceRoot":"","sources":["../../src/middlewares/RpcFallbackMiddleware.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAEV,qBAAqB,EACrB,gBAAgB,EAKhB,UAAU,EAEX,MAAM,aAAa,CAAC;AAQrB,MAAM,MAAM,4BAA4B,GAAG;IACzC,gDAAgD;IAChD,aAAa,EAAE,gBAAgB,CAAC;IAChC;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,OAAO,CAAC;IACnC,2FAA2F;IAC3F,cAAc,EAAE,MAAM,qBAAqB,CAAC;CAC7C,CAAC;AAIF;;;;;;;;;;GAUG;AACH,qBAAa,qBAAqB;;IAChC,QAAQ,CAAC,IAAI,2BAAmB;IAQhC,YAAY,OAAO,EAAE,4BAA4B,EAKhD;IAED,OAAO,IAAI,MAAM,CAEhB;IAED,IAAI,gBAAgB,IAAI,UAAU,CAOjC;CA0JF"}
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"}
@@ -2,153 +2,136 @@ 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 { filterFailedChainBalances } from '../utils/filterFailedChainBalances.js';
6
5
  import { mergeDataResponses } from './ParallelMiddleware.js';
7
6
  const CONTROLLER_NAME = 'RpcFallbackMiddleware';
8
7
  const log = createModuleLogger(projectLogger, CONTROLLER_NAME);
9
- const noopNext = async (ctx) => ctx;
10
8
  /**
11
- * RpcFallbackMiddleware recovers what upstream sources left outstanding on RPC.
9
+ * RpcFallbackMiddleware re-reads balances from the RPC data source in two
10
+ * cases:
12
11
  *
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).
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.
15
21
  *
16
- * Accounts API v6: one RPC retry on the chain axis. Chains in
17
- * `response.errors` are refetched in full (native + existing balances + pins +
18
- * default tracked assets). RPC reads those from state; the request is not
19
- * scoped via `customAssets`.
22
+ * Place this immediately after `createParallelBalanceMiddleware` in the fast
23
+ * pipeline, after `CustomAssetGraduationMiddleware` so the custom assets RPC
24
+ * carries never trigger graduation.
20
25
  */
21
26
  export class RpcFallbackMiddleware {
22
27
  name = CONTROLLER_NAME;
23
28
  #rpcDataSource;
24
- #isBalanceV6Enabled;
25
- #getAssetsState;
26
29
  constructor(options) {
27
30
  this.#rpcDataSource = options.rpcDataSource;
28
- this.#isBalanceV6Enabled =
29
- options.isBalanceV6Enabled ?? (() => false);
30
- this.#getAssetsState = options.getAssetsState;
31
31
  }
32
32
  getName() {
33
33
  return this.name;
34
34
  }
35
35
  get assetsMiddleware() {
36
36
  return forDataTypes(['balance'], async (ctx, next) => {
37
- if (this.#isBalanceV6Enabled()) {
38
- return this.#recoverV6(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);
39
50
  }
40
- return this.#recoverV5(ctx, next);
41
- });
42
- }
43
- async #recoverV6(ctx, next) {
44
- const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
45
- const chainsToFetch = ctx.request.chainIds.filter((id) => erroredChains.has(id));
46
- if (chainsToFetch.length === 0) {
47
- return next(ctx);
48
- }
49
- const rpcResult = await this.#rpcDataSource.assetsMiddleware({
50
- ...ctx,
51
- request: { ...ctx.request, chainIds: chainsToFetch },
52
- response: {},
53
- }, noopNext);
54
- const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
55
- const rpcAssetsBalance = filterFailedChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
56
- const merged = mergeDataResponses([
57
- ctx.response,
58
- {
59
- ...rpcResult.response,
60
- assetsBalance: rpcAssetsBalance,
61
- },
62
- ]);
63
- if (merged.errors && rpcAssetsBalance) {
64
- const chainsRecoveredByRpc = new Set();
65
- for (const accountBalances of Object.values(rpcAssetsBalance)) {
66
- for (const assetId of Object.keys(accountBalances)) {
67
- chainsRecoveredByRpc.add(assetId.split('/')[0]);
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
+ }
68
102
  }
69
- }
70
- for (const chainId of erroredChains) {
71
- if (chainsRecoveredByRpc.has(chainId)) {
72
- delete merged.errors[chainId];
103
+ for (const chainId of erroredChains) {
104
+ if (chainsRecoveredByRpc.has(chainId)) {
105
+ delete merged.errors[chainId];
106
+ }
73
107
  }
74
108
  }
75
- }
76
- return next({ ...ctx, response: merged });
77
- }
78
- async #recoverV5(ctx, next) {
79
- const erroredChains = new Set(Object.keys(ctx.response.errors ?? {}));
80
- const staleAssets = collectStaleTrackedAssets(ctx, this.#getAssetsState());
81
- const chainsToFetch = [
82
- ...new Set([
83
- ...ctx.request.chainIds.filter((id) => erroredChains.has(id)),
84
- // Already restricted to requested chains. Their chain may not be
85
- // errored: the Accounts API can answer for a chain while omitting a
86
- // token it does not index.
87
- ...staleAssets.map((assetId) => assetId.split('/')[0]),
88
- ]),
89
- ];
90
- if (chainsToFetch.length === 0) {
91
- return next(ctx);
92
- }
93
- log('Re-reading balances on RPC', {
94
- erroredChains: [...erroredChains],
95
- staleAssets,
96
- chains: chainsToFetch,
109
+ return next({ ...ctx, response: merged });
97
110
  });
98
- const filteredRequest = {
99
- ...ctx.request,
100
- chainIds: chainsToFetch,
101
- customAssets: [
102
- ...new Set([...(ctx.request.customAssets ?? []), ...staleAssets]),
103
- ],
104
- };
105
- const rpcResult = await this.#rpcDataSource.assetsMiddleware({
106
- ...ctx,
107
- request: filteredRequest,
108
- response: {},
109
- }, noopNext);
110
- // A chain RPC itself failed on contributed nothing trustworthy: its
111
- // balances are failure stubs (native 0) that would overwrite correct
112
- // upstream amounts and, with replaceCoveredChainBalances, wipe the
113
- // chain's token slice from state. Drop them before merging — this also
114
- // keeps failed chains from counting as "recovered" below.
115
- const rpcFailedChains = new Set(Object.keys(rpcResult.response.errors ?? {}));
116
- const rpcAssetsBalance = filterFailedChainBalances(rpcResult.response.assetsBalance, rpcFailedChains);
117
- // RPC errors are kept only for chains that were already errored
118
- // upstream. For chains fetched solely for stale tracked assets the
119
- // upstream response succeeded and stays authoritative — the stale asset
120
- // keeps its previous amount and is retried on the next pass.
121
- const rpcErrors = Object.fromEntries(Object.entries(rpcResult.response.errors ?? {}).filter(([chainId]) => erroredChains.has(chainId)));
122
- const merged = mergeDataResponses([
123
- ctx.response,
124
- {
125
- ...rpcResult.response,
126
- assetsBalance: rpcAssetsBalance,
127
- errors: rpcErrors,
128
- },
129
- ]);
130
- // Clear errors only for chains RPC actually recovered a balance for.
131
- // We must inspect the (filtered) RPC balances — NOT merged — because
132
- // merged also contains balances from the upstream sources (AccountsApi /
133
- // Websocket / Staked). If those sources returned partial data for
134
- // a chain that they also flagged as errored (e.g. via
135
- // unprocessedNetworks), and RPC then failed for that same chain,
136
- // looking at merged would incorrectly mark the error as recovered.
137
- if (merged.errors && rpcAssetsBalance) {
138
- const chainsRecoveredByRpc = new Set();
139
- for (const accountBalances of Object.values(rpcAssetsBalance)) {
140
- for (const assetId of Object.keys(accountBalances)) {
141
- chainsRecoveredByRpc.add(assetId.split('/')[0]);
142
- }
143
- }
144
- for (const chainId of erroredChains) {
145
- if (chainsRecoveredByRpc.has(chainId)) {
146
- delete merged.errors[chainId];
147
- }
148
- }
111
+ }
112
+ }
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
+ */
123
+ function filterOutChainBalances(assetsBalance, chainIds) {
124
+ if (!assetsBalance || chainIds.size === 0) {
125
+ return assetsBalance;
126
+ }
127
+ const filtered = {};
128
+ for (const [accountId, accountBalances] of Object.entries(assetsBalance)) {
129
+ const kept = Object.fromEntries(Object.entries(accountBalances).filter(([assetId]) => !chainIds.has(assetId.split('/')[0])));
130
+ if (Object.keys(kept).length > 0) {
131
+ filtered[accountId] = kept;
149
132
  }
150
- return next({ ...ctx, response: merged });
151
133
  }
134
+ return filtered;
152
135
  }
153
136
  /**
154
137
  * EVM assets tracked in state that this balance response left empty and RPC
@@ -159,11 +142,10 @@ export class RpcFallbackMiddleware {
159
142
  * set, so anything else would be queued and then silently dropped.
160
143
  *
161
144
  * @param ctx - Pipeline context.
162
- * @param state - Current AssetsController state.
163
145
  * @returns Asset IDs to hand to the RPC data source.
164
146
  */
165
- function collectStaleTrackedAssets(ctx, state) {
166
- const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } = state;
147
+ function collectStaleTrackedAssets(ctx) {
148
+ const { assetsBalance: stateAssetsBalance, customAssets: stateCustomAssets } = ctx.getAssetsState();
167
149
  const staleAssets = new Set();
168
150
  for (const { account, supportedChains } of ctx.request
169
151
  .accountsWithSupportedChains) {