@metamask/assets-controller 9.0.2 → 9.1.0

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 (56) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/dist/AssetsController.cjs +6 -15
  3. package/dist/AssetsController.cjs.map +1 -1
  4. package/dist/AssetsController.d.cts.map +1 -1
  5. package/dist/AssetsController.d.mts.map +1 -1
  6. package/dist/AssetsController.mjs +6 -15
  7. package/dist/AssetsController.mjs.map +1 -1
  8. package/dist/data-sources/AccountsApiDataSource.cjs +1 -1
  9. package/dist/data-sources/AccountsApiDataSource.cjs.map +1 -1
  10. package/dist/data-sources/AccountsApiDataSource.mjs +1 -1
  11. package/dist/data-sources/AccountsApiDataSource.mjs.map +1 -1
  12. package/dist/data-sources/PriceDataSource.cjs +64 -13
  13. package/dist/data-sources/PriceDataSource.cjs.map +1 -1
  14. package/dist/data-sources/PriceDataSource.d.cts +13 -0
  15. package/dist/data-sources/PriceDataSource.d.cts.map +1 -1
  16. package/dist/data-sources/PriceDataSource.d.mts +13 -0
  17. package/dist/data-sources/PriceDataSource.d.mts.map +1 -1
  18. package/dist/data-sources/PriceDataSource.mjs +64 -13
  19. package/dist/data-sources/PriceDataSource.mjs.map +1 -1
  20. package/dist/data-sources/SnapDataSource.cjs +2 -2
  21. package/dist/data-sources/SnapDataSource.cjs.map +1 -1
  22. package/dist/data-sources/SnapDataSource.mjs +2 -2
  23. package/dist/data-sources/SnapDataSource.mjs.map +1 -1
  24. package/dist/index.cjs +3 -1
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +2 -2
  27. package/dist/index.d.cts.map +1 -1
  28. package/dist/index.d.mts +2 -2
  29. package/dist/index.d.mts.map +1 -1
  30. package/dist/index.mjs +1 -1
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/middlewares/DetectionMiddleware.cjs +43 -23
  33. package/dist/middlewares/DetectionMiddleware.cjs.map +1 -1
  34. package/dist/middlewares/DetectionMiddleware.d.cts +12 -13
  35. package/dist/middlewares/DetectionMiddleware.d.cts.map +1 -1
  36. package/dist/middlewares/DetectionMiddleware.d.mts +12 -13
  37. package/dist/middlewares/DetectionMiddleware.d.mts.map +1 -1
  38. package/dist/middlewares/DetectionMiddleware.mjs +43 -23
  39. package/dist/middlewares/DetectionMiddleware.mjs.map +1 -1
  40. package/dist/selectors/balance.cjs +191 -8
  41. package/dist/selectors/balance.cjs.map +1 -1
  42. package/dist/selectors/balance.d.cts +80 -0
  43. package/dist/selectors/balance.d.cts.map +1 -1
  44. package/dist/selectors/balance.d.mts +80 -0
  45. package/dist/selectors/balance.d.mts.map +1 -1
  46. package/dist/selectors/balance.mjs +186 -7
  47. package/dist/selectors/balance.mjs.map +1 -1
  48. package/dist/utils/dedupingBatchFetcher.cjs +157 -0
  49. package/dist/utils/dedupingBatchFetcher.cjs.map +1 -0
  50. package/dist/utils/dedupingBatchFetcher.d.cts +65 -0
  51. package/dist/utils/dedupingBatchFetcher.d.cts.map +1 -0
  52. package/dist/utils/dedupingBatchFetcher.d.mts +65 -0
  53. package/dist/utils/dedupingBatchFetcher.d.mts.map +1 -0
  54. package/dist/utils/dedupingBatchFetcher.mjs +153 -0
  55. package/dist/utils/dedupingBatchFetcher.mjs.map +1 -0
  56. package/package.json +5 -5
@@ -16,16 +16,14 @@ const CONTROLLER_NAME = 'DetectionMiddleware';
16
16
  * DetectionMiddleware builds the set of assets that downstream sources use for
17
17
  * metadata and price fetching.
18
18
  *
19
- * This middleware:
20
- * - Includes every asset that appears in response.assetsBalance (so prices and
21
- * metadata are fetched for existing assets as well as new ones)
22
- * - Includes each account's custom assets from state (so custom tokens get
23
- * metadata and prices even when they have no balance yet)
24
- * - Fills response.detectedAssets with these asset IDs per account
25
- *
26
- * TokenDataSource and PriceDataSource both key off detectedAssets. TokenDataSource
27
- * then filters to only fetch metadata for assets that lack it; PriceDataSource
28
- * fetches prices for all detected assets.
19
+ * An asset is included in `detectedAssets` only when it is genuinely new:
20
+ * - Assets from `response.assetsBalance` are included only if they are absent
21
+ * from BOTH `state.assetsBalance` (never tracked before) AND `state.assetsInfo`
22
+ * (no metadata yet). Assets already present in either collection are considered
23
+ * known and are intentionally excluded PriceDataSource's own subscription
24
+ * handles periodic refreshes for those.
25
+ * - Each account's custom assets from state are always included because they
26
+ * may have no balance yet and are explicitly managed by the user.
29
27
  *
30
28
  * Usage:
31
29
  * ```typescript
@@ -44,47 +42,69 @@ class DetectionMiddleware {
44
42
  * Get the middleware that builds detectedAssets for metadata and price fetching.
45
43
  *
46
44
  * This middleware:
47
- * 1. Includes all assets from response.assetsBalance (so prices are fetched for existing assets too)
48
- * 2. Merges each account's custom assets from state
49
- * 3. Fills response.detectedAssets with these asset IDs per account
45
+ * 1. Includes assets from response.assetsBalance that are absent from both
46
+ * state.assetsBalance and state.assetsInfo (brand-new assets only)
47
+ * 2. Always includes each account's custom assets from state
48
+ * 3. Fills response.detectedAssets with the resulting asset IDs per account
50
49
  *
51
50
  * @returns The middleware function for the assets pipeline.
52
51
  */
53
52
  get assetsMiddleware() {
54
53
  return (0, types_1.forDataTypes)(['balance'], async (ctx, next) => {
55
54
  const { request, response } = ctx;
56
- // Get state for custom assets
55
+ // Get state for custom assets, existing balances, and existing metadata
57
56
  const state = ctx.getAssetsState();
58
- const { customAssets: stateCustomAssets } = state;
57
+ const { customAssets: stateCustomAssets, assetsBalance: stateAssetsBalance, assetsInfo: stateAssetsInfo, } = state;
59
58
  const detectedAssets = {};
60
- // 1. From balance response: include every asset with balance (so prices + metadata path include existing assets)
59
+ // 1. From balance response: only include assets that are genuinely new
60
+ // not already present in state.assetsBalance or state.assetsInfo.
61
61
  if (response.assetsBalance) {
62
62
  for (const [accountId, accountBalances] of Object.entries(response.assetsBalance)) {
63
63
  const detected = [];
64
+ const stateAccountBalances = stateAssetsBalance[accountId] ?? {};
64
65
  for (const assetId of Object.keys(accountBalances)) {
65
- detected.push(assetId);
66
+ const caipAssetId = assetId;
67
+ // Skip if already tracked in state balances or already has metadata
68
+ if (stateAccountBalances[caipAssetId] !== undefined ||
69
+ stateAssetsInfo[caipAssetId] !== undefined) {
70
+ continue;
71
+ }
72
+ detected.push(caipAssetId);
66
73
  }
67
- // Merge this account's custom assets from state
74
+ // Merge custom assets for this account, applying the same filter:
75
+ // skip if already in state balance or already has metadata.
68
76
  const customForAccount = stateCustomAssets?.[accountId] ?? [];
69
77
  for (const assetId of customForAccount) {
70
- if (!detected.includes(assetId)) {
71
- detected.push(assetId);
78
+ if (detected.includes(assetId)) {
79
+ continue;
72
80
  }
81
+ if (stateAccountBalances[assetId] !== undefined ||
82
+ stateAssetsInfo[assetId] !== undefined) {
83
+ continue;
84
+ }
85
+ detected.push(assetId);
73
86
  }
74
87
  if (detected.length > 0) {
75
88
  detectedAssets[accountId] = detected;
76
89
  }
77
90
  }
78
91
  }
79
- // 2. Accounts in request that weren't in balance response: include their custom assets
92
+ // 2. Accounts in request that weren't in balance response: include their
93
+ // custom assets that are not yet in state.
80
94
  for (const { account } of request.accountsWithSupportedChains) {
81
95
  const accountId = account.id;
82
96
  if (detectedAssets[accountId]) {
83
97
  continue;
84
98
  }
99
+ const stateAccountBalances = stateAssetsBalance[accountId] ?? {};
85
100
  const customForAccount = stateCustomAssets?.[accountId] ?? [];
86
- if (customForAccount.length > 0) {
87
- detectedAssets[accountId] = customForAccount;
101
+ const newCustomAssets = customForAccount.filter((assetId) => {
102
+ const inBalance = stateAccountBalances[assetId] !== undefined;
103
+ const inInfo = stateAssetsInfo[assetId] !== undefined;
104
+ return !inBalance && !inInfo;
105
+ });
106
+ if (newCustomAssets.length > 0) {
107
+ detectedAssets[accountId] = newCustomAssets;
88
108
  }
89
109
  }
90
110
  if (Object.keys(detectedAssets).length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"DetectionMiddleware.cjs","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":";;;AAAA,0CAA8D;AAC9D,wCAAwC;AAGxC,+EAA+E;AAC/E,YAAY;AACZ,+EAA+E;AAE/E,MAAM,eAAe,GAAG,qBAAqB,CAAC;AAE9C,uBAAuB;AACvB,IAAA,2BAAkB,EAAC,sBAAa,EAAE,eAAe,CAAC,CAAC;AAEnD,+EAA+E;AAC/E,uBAAuB;AACvB,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAa,mBAAmB;IAAhC;QACW,SAAI,GAAG,eAAe,CAAC;IAwElC,CAAC;IAtEC,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;;;;;;;OASG;IACH,IAAI,gBAAgB;QAClB,OAAO,IAAA,oBAAY,EAAC,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACnD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC;YAElC,8BAA8B;YAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,cAAc,EAAE,CAAC;YACnC,MAAM,EAAE,YAAY,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAC;YAElD,MAAM,cAAc,GAAuC,EAAE,CAAC;YAE9D,iHAAiH;YACjH,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;gBAC3B,KAAK,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,IAAI,MAAM,CAAC,OAAO,CACvD,QAAQ,CAAC,aAAa,CACvB,EAAE,CAAC;oBACF,MAAM,QAAQ,GAAoB,EAAE,CAAC;oBAErC,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAC/B,eAA0C,CAC3C,EAAE,CAAC;wBACF,QAAQ,CAAC,IAAI,CAAC,OAAwB,CAAC,CAAC;oBAC1C,CAAC;oBAED,gDAAgD;oBAChD,MAAM,gBAAgB,GAAG,iBAAiB,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;oBAC9D,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;wBACvC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;4BAChC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;wBACzB,CAAC;oBACH,CAAC;oBAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACxB,cAAc,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC;oBACvC,CAAC;gBACH,CAAC;YACH,CAAC;YAED,uFAAuF;YACvF,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,OAAO,CAAC,2BAA2B,EAAE,CAAC;gBAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,CAAC;gBAC7B,IAAI,cAAc,CAAC,SAAS,CAAC,EAAE,CAAC;oBAC9B,SAAS;gBACX,CAAC;gBACD,MAAM,gBAAgB,GAAG,iBAAiB,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;gBAC9D,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAChC,cAAc,CAAC,SAAS,CAAC,GAAG,gBAAgB,CAAC;gBAC/C,CAAC;YACH,CAAC;YAED,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3C,QAAQ,CAAC,cAAc,GAAG,cAAc,CAAC;YAC3C,CAAC;YAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAzED,kDAyEC","sourcesContent":["import { projectLogger, createModuleLogger } from '../logger';\nimport { forDataTypes } from '../types';\nimport type { AccountId, Caip19AssetId, Middleware } from '../types';\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 * This middleware:\n * - Includes every asset that appears in response.assetsBalance (so prices and\n * metadata are fetched for existing assets as well as new ones)\n * - Includes each account's custom assets from state (so custom tokens get\n * metadata and prices even when they have no balance yet)\n * - Fills response.detectedAssets with these asset IDs per account\n *\n * TokenDataSource and PriceDataSource both key off detectedAssets. TokenDataSource\n * then filters to only fetch metadata for assets that lack it; PriceDataSource\n * fetches prices for all detected assets.\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 all assets from response.assetsBalance (so prices are fetched for existing assets too)\n * 2. Merges each account's custom assets from state\n * 3. Fills response.detectedAssets with these asset IDs per account\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\n const state = ctx.getAssetsState();\n const { customAssets: stateCustomAssets } = state;\n\n const detectedAssets: Record<AccountId, Caip19AssetId[]> = {};\n\n // 1. From balance response: include every asset with balance (so prices + metadata path include existing assets)\n if (response.assetsBalance) {\n for (const [accountId, accountBalances] of Object.entries(\n response.assetsBalance,\n )) {\n const detected: Caip19AssetId[] = [];\n\n for (const assetId of Object.keys(\n accountBalances as Record<string, unknown>,\n )) {\n detected.push(assetId as Caip19AssetId);\n }\n\n // Merge this account's custom assets from state\n const customForAccount = stateCustomAssets?.[accountId] ?? [];\n for (const assetId of customForAccount) {\n if (!detected.includes(assetId)) {\n detected.push(assetId);\n }\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 custom assets\n for (const { account } of request.accountsWithSupportedChains) {\n const accountId = account.id;\n if (detectedAssets[accountId]) {\n continue;\n }\n const customForAccount = stateCustomAssets?.[accountId] ?? [];\n if (customForAccount.length > 0) {\n detectedAssets[accountId] = customForAccount;\n }\n }\n\n if (Object.keys(detectedAssets).length > 0) {\n response.detectedAssets = detectedAssets;\n }\n\n return next(ctx);\n });\n }\n}\n"]}
1
+ {"version":3,"file":"DetectionMiddleware.cjs","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":";;;AAAA,0CAA8D;AAC9D,wCAAwC;AAGxC,+EAA+E;AAC/E,YAAY;AACZ,+EAA+E;AAE/E,MAAM,eAAe,GAAG,qBAAqB,CAAC;AAE9C,uBAAuB;AACvB,IAAA,2BAAkB,EAAC,sBAAa,EAAE,eAAe,CAAC,CAAC;AAEnD,+EAA+E;AAC/E,uBAAuB;AACvB,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,mBAAmB;IAAhC;QACW,SAAI,GAAG,eAAe,CAAC;IAuGlC,CAAC;IArGC,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;;;;;;;;OAUG;IACH,IAAI,gBAAgB;QAClB,OAAO,IAAA,oBAAY,EAAC,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACnD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC;YAElC,wEAAwE;YACxE,MAAM,KAAK,GAAG,GAAG,CAAC,cAAc,EAAE,CAAC;YACnC,MAAM,EACJ,YAAY,EAAE,iBAAiB,EAC/B,aAAa,EAAE,kBAAkB,EACjC,UAAU,EAAE,eAAe,GAC5B,GAAG,KAAK,CAAC;YAEV,MAAM,cAAc,GAAuC,EAAE,CAAC;YAE9D,yEAAyE;YACzE,qEAAqE;YACrE,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;gBAC3B,KAAK,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,IAAI,MAAM,CAAC,OAAO,CACvD,QAAQ,CAAC,aAAa,CACvB,EAAE,CAAC;oBACF,MAAM,QAAQ,GAAoB,EAAE,CAAC;oBAErC,MAAM,oBAAoB,GAAG,kBAAkB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;oBAEjE,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAC/B,eAA0C,CAC3C,EAAE,CAAC;wBACF,MAAM,WAAW,GAAG,OAAwB,CAAC;wBAC7C,oEAAoE;wBACpE,IACE,oBAAoB,CAAC,WAAW,CAAC,KAAK,SAAS;4BAC/C,eAAe,CAAC,WAAW,CAAC,KAAK,SAAS,EAC1C,CAAC;4BACD,SAAS;wBACX,CAAC;wBACD,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;oBAC7B,CAAC;oBAED,kEAAkE;oBAClE,4DAA4D;oBAC5D,MAAM,gBAAgB,GAAG,iBAAiB,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;oBAC9D,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;wBACvC,IAAI,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;4BAC/B,SAAS;wBACX,CAAC;wBACD,IACE,oBAAoB,CAAC,OAAO,CAAC,KAAK,SAAS;4BAC3C,eAAe,CAAC,OAAO,CAAC,KAAK,SAAS,EACtC,CAAC;4BACD,SAAS;wBACX,CAAC;wBACD,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBACzB,CAAC;oBAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACxB,cAAc,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC;oBACvC,CAAC;gBACH,CAAC;YACH,CAAC;YAED,yEAAyE;YACzE,8CAA8C;YAC9C,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,OAAO,CAAC,2BAA2B,EAAE,CAAC;gBAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,CAAC;gBAC7B,IAAI,cAAc,CAAC,SAAS,CAAC,EAAE,CAAC;oBAC9B,SAAS;gBACX,CAAC;gBACD,MAAM,oBAAoB,GAAG,kBAAkB,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;gBACjE,MAAM,gBAAgB,GAAG,iBAAiB,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;gBAC9D,MAAM,eAAe,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE;oBAC1D,MAAM,SAAS,GAAG,oBAAoB,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC;oBAC9D,MAAM,MAAM,GAAG,eAAe,CAAC,OAAO,CAAC,KAAK,SAAS,CAAC;oBACtD,OAAO,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC;gBAC/B,CAAC,CAAC,CAAC;gBACH,IAAI,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC/B,cAAc,CAAC,SAAS,CAAC,GAAG,eAAe,CAAC;gBAC9C,CAAC;YACH,CAAC;YAED,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3C,QAAQ,CAAC,cAAc,GAAG,cAAc,CAAC;YAC3C,CAAC;YAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAxGD,kDAwGC","sourcesContent":["import { projectLogger, createModuleLogger } from '../logger';\nimport { forDataTypes } from '../types';\nimport type { AccountId, Caip19AssetId, Middleware } from '../types';\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 *\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 } = 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 return next(ctx);\n });\n }\n}\n"]}
@@ -3,16 +3,14 @@ import type { Middleware } from "../types.cjs";
3
3
  * DetectionMiddleware builds the set of assets that downstream sources use for
4
4
  * metadata and price fetching.
5
5
  *
6
- * This middleware:
7
- * - Includes every asset that appears in response.assetsBalance (so prices and
8
- * metadata are fetched for existing assets as well as new ones)
9
- * - Includes each account's custom assets from state (so custom tokens get
10
- * metadata and prices even when they have no balance yet)
11
- * - Fills response.detectedAssets with these asset IDs per account
12
- *
13
- * TokenDataSource and PriceDataSource both key off detectedAssets. TokenDataSource
14
- * then filters to only fetch metadata for assets that lack it; PriceDataSource
15
- * fetches prices for all detected assets.
6
+ * An asset is included in `detectedAssets` only when it is genuinely new:
7
+ * - Assets from `response.assetsBalance` are included only if they are absent
8
+ * from BOTH `state.assetsBalance` (never tracked before) AND `state.assetsInfo`
9
+ * (no metadata yet). Assets already present in either collection are considered
10
+ * known and are intentionally excluded PriceDataSource's own subscription
11
+ * handles periodic refreshes for those.
12
+ * - Each account's custom assets from state are always included because they
13
+ * may have no balance yet and are explicitly managed by the user.
16
14
  *
17
15
  * Usage:
18
16
  * ```typescript
@@ -27,9 +25,10 @@ export declare class DetectionMiddleware {
27
25
  * Get the middleware that builds detectedAssets for metadata and price fetching.
28
26
  *
29
27
  * This middleware:
30
- * 1. Includes all assets from response.assetsBalance (so prices are fetched for existing assets too)
31
- * 2. Merges each account's custom assets from state
32
- * 3. Fills response.detectedAssets with these asset IDs per account
28
+ * 1. Includes assets from response.assetsBalance that are absent from both
29
+ * state.assetsBalance and state.assetsInfo (brand-new assets only)
30
+ * 2. Always includes each account's custom assets from state
31
+ * 3. Fills response.detectedAssets with the resulting asset IDs per account
33
32
  *
34
33
  * @returns The middleware function for the assets pipeline.
35
34
  */
@@ -1 +1 @@
1
- {"version":3,"file":"DetectionMiddleware.d.cts","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA4B,UAAU,EAAE,qBAAiB;AAerE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,IAAI,yBAAmB;IAEhC,OAAO,IAAI,MAAM;IAIjB;;;;;;;;;OASG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAuDjC;CACF"}
1
+ {"version":3,"file":"DetectionMiddleware.d.cts","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA4B,UAAU,EAAE,qBAAiB;AAerE;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,IAAI,yBAAmB;IAEhC,OAAO,IAAI,MAAM;IAIjB;;;;;;;;;;OAUG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAqFjC;CACF"}
@@ -3,16 +3,14 @@ import type { Middleware } from "../types.mjs";
3
3
  * DetectionMiddleware builds the set of assets that downstream sources use for
4
4
  * metadata and price fetching.
5
5
  *
6
- * This middleware:
7
- * - Includes every asset that appears in response.assetsBalance (so prices and
8
- * metadata are fetched for existing assets as well as new ones)
9
- * - Includes each account's custom assets from state (so custom tokens get
10
- * metadata and prices even when they have no balance yet)
11
- * - Fills response.detectedAssets with these asset IDs per account
12
- *
13
- * TokenDataSource and PriceDataSource both key off detectedAssets. TokenDataSource
14
- * then filters to only fetch metadata for assets that lack it; PriceDataSource
15
- * fetches prices for all detected assets.
6
+ * An asset is included in `detectedAssets` only when it is genuinely new:
7
+ * - Assets from `response.assetsBalance` are included only if they are absent
8
+ * from BOTH `state.assetsBalance` (never tracked before) AND `state.assetsInfo`
9
+ * (no metadata yet). Assets already present in either collection are considered
10
+ * known and are intentionally excluded PriceDataSource's own subscription
11
+ * handles periodic refreshes for those.
12
+ * - Each account's custom assets from state are always included because they
13
+ * may have no balance yet and are explicitly managed by the user.
16
14
  *
17
15
  * Usage:
18
16
  * ```typescript
@@ -27,9 +25,10 @@ export declare class DetectionMiddleware {
27
25
  * Get the middleware that builds detectedAssets for metadata and price fetching.
28
26
  *
29
27
  * This middleware:
30
- * 1. Includes all assets from response.assetsBalance (so prices are fetched for existing assets too)
31
- * 2. Merges each account's custom assets from state
32
- * 3. Fills response.detectedAssets with these asset IDs per account
28
+ * 1. Includes assets from response.assetsBalance that are absent from both
29
+ * state.assetsBalance and state.assetsInfo (brand-new assets only)
30
+ * 2. Always includes each account's custom assets from state
31
+ * 3. Fills response.detectedAssets with the resulting asset IDs per account
33
32
  *
34
33
  * @returns The middleware function for the assets pipeline.
35
34
  */
@@ -1 +1 @@
1
- {"version":3,"file":"DetectionMiddleware.d.mts","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA4B,UAAU,EAAE,qBAAiB;AAerE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,IAAI,yBAAmB;IAEhC,OAAO,IAAI,MAAM;IAIjB;;;;;;;;;OASG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAuDjC;CACF"}
1
+ {"version":3,"file":"DetectionMiddleware.d.mts","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA4B,UAAU,EAAE,qBAAiB;AAerE;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,IAAI,yBAAmB;IAEhC,OAAO,IAAI,MAAM;IAIjB;;;;;;;;;;OAUG;IACH,IAAI,gBAAgB,IAAI,UAAU,CAqFjC;CACF"}
@@ -13,16 +13,14 @@ createModuleLogger(projectLogger, CONTROLLER_NAME);
13
13
  * DetectionMiddleware builds the set of assets that downstream sources use for
14
14
  * metadata and price fetching.
15
15
  *
16
- * This middleware:
17
- * - Includes every asset that appears in response.assetsBalance (so prices and
18
- * metadata are fetched for existing assets as well as new ones)
19
- * - Includes each account's custom assets from state (so custom tokens get
20
- * metadata and prices even when they have no balance yet)
21
- * - Fills response.detectedAssets with these asset IDs per account
22
- *
23
- * TokenDataSource and PriceDataSource both key off detectedAssets. TokenDataSource
24
- * then filters to only fetch metadata for assets that lack it; PriceDataSource
25
- * fetches prices for all detected assets.
16
+ * An asset is included in `detectedAssets` only when it is genuinely new:
17
+ * - Assets from `response.assetsBalance` are included only if they are absent
18
+ * from BOTH `state.assetsBalance` (never tracked before) AND `state.assetsInfo`
19
+ * (no metadata yet). Assets already present in either collection are considered
20
+ * known and are intentionally excluded PriceDataSource's own subscription
21
+ * handles periodic refreshes for those.
22
+ * - Each account's custom assets from state are always included because they
23
+ * may have no balance yet and are explicitly managed by the user.
26
24
  *
27
25
  * Usage:
28
26
  * ```typescript
@@ -41,47 +39,69 @@ export class DetectionMiddleware {
41
39
  * Get the middleware that builds detectedAssets for metadata and price fetching.
42
40
  *
43
41
  * This middleware:
44
- * 1. Includes all assets from response.assetsBalance (so prices are fetched for existing assets too)
45
- * 2. Merges each account's custom assets from state
46
- * 3. Fills response.detectedAssets with these asset IDs per account
42
+ * 1. Includes assets from response.assetsBalance that are absent from both
43
+ * state.assetsBalance and state.assetsInfo (brand-new assets only)
44
+ * 2. Always includes each account's custom assets from state
45
+ * 3. Fills response.detectedAssets with the resulting asset IDs per account
47
46
  *
48
47
  * @returns The middleware function for the assets pipeline.
49
48
  */
50
49
  get assetsMiddleware() {
51
50
  return forDataTypes(['balance'], async (ctx, next) => {
52
51
  const { request, response } = ctx;
53
- // Get state for custom assets
52
+ // Get state for custom assets, existing balances, and existing metadata
54
53
  const state = ctx.getAssetsState();
55
- const { customAssets: stateCustomAssets } = state;
54
+ const { customAssets: stateCustomAssets, assetsBalance: stateAssetsBalance, assetsInfo: stateAssetsInfo, } = state;
56
55
  const detectedAssets = {};
57
- // 1. From balance response: include every asset with balance (so prices + metadata path include existing assets)
56
+ // 1. From balance response: only include assets that are genuinely new
57
+ // not already present in state.assetsBalance or state.assetsInfo.
58
58
  if (response.assetsBalance) {
59
59
  for (const [accountId, accountBalances] of Object.entries(response.assetsBalance)) {
60
60
  const detected = [];
61
+ const stateAccountBalances = stateAssetsBalance[accountId] ?? {};
61
62
  for (const assetId of Object.keys(accountBalances)) {
62
- detected.push(assetId);
63
+ const caipAssetId = assetId;
64
+ // Skip if already tracked in state balances or already has metadata
65
+ if (stateAccountBalances[caipAssetId] !== undefined ||
66
+ stateAssetsInfo[caipAssetId] !== undefined) {
67
+ continue;
68
+ }
69
+ detected.push(caipAssetId);
63
70
  }
64
- // Merge this account's custom assets from state
71
+ // Merge custom assets for this account, applying the same filter:
72
+ // skip if already in state balance or already has metadata.
65
73
  const customForAccount = stateCustomAssets?.[accountId] ?? [];
66
74
  for (const assetId of customForAccount) {
67
- if (!detected.includes(assetId)) {
68
- detected.push(assetId);
75
+ if (detected.includes(assetId)) {
76
+ continue;
69
77
  }
78
+ if (stateAccountBalances[assetId] !== undefined ||
79
+ stateAssetsInfo[assetId] !== undefined) {
80
+ continue;
81
+ }
82
+ detected.push(assetId);
70
83
  }
71
84
  if (detected.length > 0) {
72
85
  detectedAssets[accountId] = detected;
73
86
  }
74
87
  }
75
88
  }
76
- // 2. Accounts in request that weren't in balance response: include their custom assets
89
+ // 2. Accounts in request that weren't in balance response: include their
90
+ // custom assets that are not yet in state.
77
91
  for (const { account } of request.accountsWithSupportedChains) {
78
92
  const accountId = account.id;
79
93
  if (detectedAssets[accountId]) {
80
94
  continue;
81
95
  }
96
+ const stateAccountBalances = stateAssetsBalance[accountId] ?? {};
82
97
  const customForAccount = stateCustomAssets?.[accountId] ?? [];
83
- if (customForAccount.length > 0) {
84
- detectedAssets[accountId] = customForAccount;
98
+ const newCustomAssets = customForAccount.filter((assetId) => {
99
+ const inBalance = stateAccountBalances[assetId] !== undefined;
100
+ const inInfo = stateAssetsInfo[assetId] !== undefined;
101
+ return !inBalance && !inInfo;
102
+ });
103
+ if (newCustomAssets.length > 0) {
104
+ detectedAssets[accountId] = newCustomAssets;
85
105
  }
86
106
  }
87
107
  if (Object.keys(detectedAssets).length > 0) {
@@ -1 +1 @@
1
- {"version":3,"file":"DetectionMiddleware.mjs","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,sBAAkB;AAC9D,OAAO,EAAE,YAAY,EAAE,qBAAiB;AAGxC,+EAA+E;AAC/E,YAAY;AACZ,+EAA+E;AAE/E,MAAM,eAAe,GAAG,qBAAqB,CAAC;AAE9C,uBAAuB;AACvB,kBAAkB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;AAEnD,+EAA+E;AAC/E,uBAAuB;AACvB,+EAA+E;AAE/E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,OAAO,mBAAmB;IAAhC;QACW,SAAI,GAAG,eAAe,CAAC;IAwElC,CAAC;IAtEC,OAAO;QACL,OAAO,IAAI,CAAC,IAAI,CAAC;IACnB,CAAC;IAED;;;;;;;;;OASG;IACH,IAAI,gBAAgB;QAClB,OAAO,YAAY,CAAC,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;YACnD,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC;YAElC,8BAA8B;YAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,cAAc,EAAE,CAAC;YACnC,MAAM,EAAE,YAAY,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAC;YAElD,MAAM,cAAc,GAAuC,EAAE,CAAC;YAE9D,iHAAiH;YACjH,IAAI,QAAQ,CAAC,aAAa,EAAE,CAAC;gBAC3B,KAAK,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,IAAI,MAAM,CAAC,OAAO,CACvD,QAAQ,CAAC,aAAa,CACvB,EAAE,CAAC;oBACF,MAAM,QAAQ,GAAoB,EAAE,CAAC;oBAErC,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,IAAI,CAC/B,eAA0C,CAC3C,EAAE,CAAC;wBACF,QAAQ,CAAC,IAAI,CAAC,OAAwB,CAAC,CAAC;oBAC1C,CAAC;oBAED,gDAAgD;oBAChD,MAAM,gBAAgB,GAAG,iBAAiB,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;oBAC9D,KAAK,MAAM,OAAO,IAAI,gBAAgB,EAAE,CAAC;wBACvC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;4BAChC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;wBACzB,CAAC;oBACH,CAAC;oBAED,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACxB,cAAc,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC;oBACvC,CAAC;gBACH,CAAC;YACH,CAAC;YAED,uFAAuF;YACvF,KAAK,MAAM,EAAE,OAAO,EAAE,IAAI,OAAO,CAAC,2BAA2B,EAAE,CAAC;gBAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,EAAE,CAAC;gBAC7B,IAAI,cAAc,CAAC,SAAS,CAAC,EAAE,CAAC;oBAC9B,SAAS;gBACX,CAAC;gBACD,MAAM,gBAAgB,GAAG,iBAAiB,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;gBAC9D,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAChC,cAAc,CAAC,SAAS,CAAC,GAAG,gBAAgB,CAAC;gBAC/C,CAAC;YACH,CAAC;YAED,IAAI,MAAM,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3C,QAAQ,CAAC,cAAc,GAAG,cAAc,CAAC;YAC3C,CAAC;YAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC,CAAC,CAAC;IACL,CAAC;CACF","sourcesContent":["import { projectLogger, createModuleLogger } from '../logger';\nimport { forDataTypes } from '../types';\nimport type { AccountId, Caip19AssetId, Middleware } from '../types';\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 * This middleware:\n * - Includes every asset that appears in response.assetsBalance (so prices and\n * metadata are fetched for existing assets as well as new ones)\n * - Includes each account's custom assets from state (so custom tokens get\n * metadata and prices even when they have no balance yet)\n * - Fills response.detectedAssets with these asset IDs per account\n *\n * TokenDataSource and PriceDataSource both key off detectedAssets. TokenDataSource\n * then filters to only fetch metadata for assets that lack it; PriceDataSource\n * fetches prices for all detected assets.\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 all assets from response.assetsBalance (so prices are fetched for existing assets too)\n * 2. Merges each account's custom assets from state\n * 3. Fills response.detectedAssets with these asset IDs per account\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\n const state = ctx.getAssetsState();\n const { customAssets: stateCustomAssets } = state;\n\n const detectedAssets: Record<AccountId, Caip19AssetId[]> = {};\n\n // 1. From balance response: include every asset with balance (so prices + metadata path include existing assets)\n if (response.assetsBalance) {\n for (const [accountId, accountBalances] of Object.entries(\n response.assetsBalance,\n )) {\n const detected: Caip19AssetId[] = [];\n\n for (const assetId of Object.keys(\n accountBalances as Record<string, unknown>,\n )) {\n detected.push(assetId as Caip19AssetId);\n }\n\n // Merge this account's custom assets from state\n const customForAccount = stateCustomAssets?.[accountId] ?? [];\n for (const assetId of customForAccount) {\n if (!detected.includes(assetId)) {\n detected.push(assetId);\n }\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 custom assets\n for (const { account } of request.accountsWithSupportedChains) {\n const accountId = account.id;\n if (detectedAssets[accountId]) {\n continue;\n }\n const customForAccount = stateCustomAssets?.[accountId] ?? [];\n if (customForAccount.length > 0) {\n detectedAssets[accountId] = customForAccount;\n }\n }\n\n if (Object.keys(detectedAssets).length > 0) {\n response.detectedAssets = detectedAssets;\n }\n\n return next(ctx);\n });\n }\n}\n"]}
1
+ {"version":3,"file":"DetectionMiddleware.mjs","sourceRoot":"","sources":["../../src/middlewares/DetectionMiddleware.ts"],"names":[],"mappings":"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","sourcesContent":["import { projectLogger, createModuleLogger } from '../logger';\nimport { forDataTypes } from '../types';\nimport type { AccountId, Caip19AssetId, Middleware } from '../types';\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 *\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 } = 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 return next(ctx);\n });\n }\n}\n"]}