@metamask-previews/assets-controller 14.0.2-preview-3866c0ff1 → 14.0.2-preview-e3275932a

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 (583) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/dist/AssetsController-method-action-types.cjs +7 -0
  3. package/dist/AssetsController-method-action-types.cjs.map +1 -0
  4. package/dist/{AssetsController-method-action-types.d.ts → AssetsController-method-action-types.d.cts} +2 -2
  5. package/dist/AssetsController-method-action-types.d.cts.map +1 -0
  6. package/dist/AssetsController-method-action-types.d.mts +181 -0
  7. package/dist/AssetsController-method-action-types.d.mts.map +1 -0
  8. package/dist/{AssetsController-method-action-types.js → AssetsController-method-action-types.mjs} +1 -1
  9. package/dist/AssetsController-method-action-types.mjs.map +1 -0
  10. package/dist/AssetsController.cjs +2596 -0
  11. package/dist/AssetsController.cjs.map +1 -0
  12. package/dist/{AssetsController.d.ts → AssetsController.d.cts} +30 -30
  13. package/dist/AssetsController.d.cts.map +1 -0
  14. package/dist/AssetsController.d.mts +395 -0
  15. package/dist/AssetsController.d.mts.map +1 -0
  16. package/dist/AssetsController.mjs +2589 -0
  17. package/dist/AssetsController.mjs.map +1 -0
  18. package/dist/data-sources/AbstractDataSource.cjs +111 -0
  19. package/dist/data-sources/AbstractDataSource.cjs.map +1 -0
  20. package/dist/data-sources/{AbstractDataSource.d.ts → AbstractDataSource.d.cts} +2 -2
  21. package/dist/data-sources/AbstractDataSource.d.cts.map +1 -0
  22. package/dist/data-sources/AbstractDataSource.d.mts +113 -0
  23. package/dist/data-sources/AbstractDataSource.d.mts.map +1 -0
  24. package/dist/data-sources/{AbstractDataSource.js → AbstractDataSource.mjs} +3 -5
  25. package/dist/data-sources/AbstractDataSource.mjs.map +1 -0
  26. package/dist/data-sources/AccountActivityDataSource.cjs +267 -0
  27. package/dist/data-sources/AccountActivityDataSource.cjs.map +1 -0
  28. package/dist/data-sources/{AccountActivityDataSource.d.ts → AccountActivityDataSource.d.cts} +6 -6
  29. package/dist/data-sources/AccountActivityDataSource.d.cts.map +1 -0
  30. package/dist/data-sources/AccountActivityDataSource.d.mts +80 -0
  31. package/dist/data-sources/AccountActivityDataSource.d.mts.map +1 -0
  32. package/dist/data-sources/AccountActivityDataSource.mjs +259 -0
  33. package/dist/data-sources/AccountActivityDataSource.mjs.map +1 -0
  34. package/dist/data-sources/AccountsApiDataSource.cjs +586 -0
  35. package/dist/data-sources/AccountsApiDataSource.cjs.map +1 -0
  36. package/dist/data-sources/{AccountsApiDataSource.d.ts → AccountsApiDataSource.d.cts} +7 -7
  37. package/dist/data-sources/AccountsApiDataSource.d.cts.map +1 -0
  38. package/dist/data-sources/AccountsApiDataSource.d.mts +82 -0
  39. package/dist/data-sources/AccountsApiDataSource.d.mts.map +1 -0
  40. package/dist/data-sources/AccountsApiDataSource.mjs +581 -0
  41. package/dist/data-sources/AccountsApiDataSource.mjs.map +1 -0
  42. package/dist/data-sources/PriceDataSource.cjs +466 -0
  43. package/dist/data-sources/PriceDataSource.cjs.map +1 -0
  44. package/dist/data-sources/{PriceDataSource.d.ts → PriceDataSource.d.cts} +5 -5
  45. package/dist/data-sources/PriceDataSource.d.cts.map +1 -0
  46. package/dist/data-sources/PriceDataSource.d.mts +100 -0
  47. package/dist/data-sources/PriceDataSource.d.mts.map +1 -0
  48. package/dist/data-sources/{PriceDataSource.js → PriceDataSource.mjs} +185 -179
  49. package/dist/data-sources/PriceDataSource.mjs.map +1 -0
  50. package/dist/data-sources/RpcDataSource.cjs +1021 -0
  51. package/dist/data-sources/RpcDataSource.cjs.map +1 -0
  52. package/dist/data-sources/{RpcDataSource.d.ts → RpcDataSource.d.cts} +10 -10
  53. package/dist/data-sources/RpcDataSource.d.cts.map +1 -0
  54. package/dist/data-sources/RpcDataSource.d.mts +174 -0
  55. package/dist/data-sources/RpcDataSource.d.mts.map +1 -0
  56. package/dist/data-sources/RpcDataSource.mjs +1012 -0
  57. package/dist/data-sources/RpcDataSource.mjs.map +1 -0
  58. package/dist/data-sources/SnapDataSource.cjs +443 -0
  59. package/dist/data-sources/SnapDataSource.cjs.map +1 -0
  60. package/dist/data-sources/{SnapDataSource.d.ts → SnapDataSource.d.cts} +8 -8
  61. package/dist/data-sources/SnapDataSource.d.cts.map +1 -0
  62. package/dist/data-sources/SnapDataSource.d.mts +127 -0
  63. package/dist/data-sources/SnapDataSource.d.mts.map +1 -0
  64. package/dist/data-sources/{SnapDataSource.js → SnapDataSource.mjs} +177 -225
  65. package/dist/data-sources/SnapDataSource.mjs.map +1 -0
  66. package/dist/data-sources/StakedBalanceDataSource.cjs +616 -0
  67. package/dist/data-sources/StakedBalanceDataSource.cjs.map +1 -0
  68. package/dist/data-sources/{StakedBalanceDataSource.d.ts → StakedBalanceDataSource.d.cts} +5 -5
  69. package/dist/data-sources/StakedBalanceDataSource.d.cts.map +1 -0
  70. package/dist/data-sources/StakedBalanceDataSource.d.mts +72 -0
  71. package/dist/data-sources/StakedBalanceDataSource.d.mts.map +1 -0
  72. package/dist/data-sources/StakedBalanceDataSource.mjs +612 -0
  73. package/dist/data-sources/StakedBalanceDataSource.mjs.map +1 -0
  74. package/dist/data-sources/TokenDataSource.cjs +583 -0
  75. package/dist/data-sources/TokenDataSource.cjs.map +1 -0
  76. package/dist/data-sources/{TokenDataSource.d.ts → TokenDataSource.d.cts} +5 -5
  77. package/dist/data-sources/TokenDataSource.d.cts.map +1 -0
  78. package/dist/data-sources/TokenDataSource.d.mts +81 -0
  79. package/dist/data-sources/TokenDataSource.d.mts.map +1 -0
  80. package/dist/data-sources/{TokenDataSource.js → TokenDataSource.mjs} +164 -157
  81. package/dist/data-sources/TokenDataSource.mjs.map +1 -0
  82. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.cjs +705 -0
  83. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.cjs.map +1 -0
  84. package/dist/data-sources/evm-rpc-services/clients/{MulticallClient.d.ts → MulticallClient.d.cts} +3 -3
  85. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.d.cts.map +1 -0
  86. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.d.mts +65 -0
  87. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.d.mts.map +1 -0
  88. package/dist/data-sources/evm-rpc-services/clients/{MulticallClient.js → MulticallClient.mjs} +160 -148
  89. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.mjs.map +1 -0
  90. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.cjs +312 -0
  91. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.cjs.map +1 -0
  92. package/dist/data-sources/evm-rpc-services/clients/{TokensApiClient.d.ts → TokensApiClient.d.cts} +2 -2
  93. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.cts.map +1 -0
  94. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.mts +61 -0
  95. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.mts.map +1 -0
  96. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.mjs +308 -0
  97. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.mjs.map +1 -0
  98. package/dist/data-sources/evm-rpc-services/clients/index.cjs +10 -0
  99. package/dist/data-sources/evm-rpc-services/clients/index.cjs.map +1 -0
  100. package/dist/data-sources/evm-rpc-services/clients/index.d.cts +4 -0
  101. package/dist/data-sources/evm-rpc-services/clients/index.d.cts.map +1 -0
  102. package/dist/data-sources/evm-rpc-services/clients/index.d.mts +4 -0
  103. package/dist/data-sources/evm-rpc-services/clients/index.d.mts.map +1 -0
  104. package/dist/data-sources/evm-rpc-services/clients/index.mjs +3 -0
  105. package/dist/data-sources/evm-rpc-services/clients/index.mjs.map +1 -0
  106. package/dist/data-sources/evm-rpc-services/index.cjs +17 -0
  107. package/dist/data-sources/evm-rpc-services/index.cjs.map +1 -0
  108. package/dist/data-sources/evm-rpc-services/{index.d.ts → index.d.cts} +5 -5
  109. package/dist/data-sources/evm-rpc-services/index.d.cts.map +1 -0
  110. package/dist/data-sources/evm-rpc-services/index.d.mts +5 -0
  111. package/dist/data-sources/evm-rpc-services/index.d.mts.map +1 -0
  112. package/dist/data-sources/evm-rpc-services/index.mjs +4 -0
  113. package/dist/data-sources/evm-rpc-services/index.mjs.map +1 -0
  114. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.cjs +247 -0
  115. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.cjs.map +1 -0
  116. package/dist/data-sources/evm-rpc-services/services/{BalanceFetcher.d.ts → BalanceFetcher.d.cts} +11 -11
  117. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.cts.map +1 -0
  118. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.mts +96 -0
  119. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.mts.map +1 -0
  120. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.mjs +243 -0
  121. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.mjs.map +1 -0
  122. package/dist/data-sources/evm-rpc-services/services/StakedBalanceFetcher.cjs +132 -0
  123. package/dist/data-sources/evm-rpc-services/services/StakedBalanceFetcher.cjs.map +1 -0
  124. package/dist/data-sources/evm-rpc-services/services/{StakedBalanceFetcher.d.ts → StakedBalanceFetcher.d.cts} +14 -13
  125. package/dist/data-sources/evm-rpc-services/services/StakedBalanceFetcher.d.cts.map +1 -0
  126. package/dist/data-sources/evm-rpc-services/services/StakedBalanceFetcher.d.mts +81 -0
  127. package/dist/data-sources/evm-rpc-services/services/StakedBalanceFetcher.d.mts.map +1 -0
  128. package/dist/data-sources/evm-rpc-services/services/{StakedBalanceFetcher.js → StakedBalanceFetcher.mjs} +27 -13
  129. package/dist/data-sources/evm-rpc-services/services/StakedBalanceFetcher.mjs.map +1 -0
  130. package/dist/data-sources/evm-rpc-services/services/TokenDetector.cjs +230 -0
  131. package/dist/data-sources/evm-rpc-services/services/TokenDetector.cjs.map +1 -0
  132. package/dist/data-sources/evm-rpc-services/services/{TokenDetector.d.ts → TokenDetector.d.cts} +12 -12
  133. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.cts.map +1 -0
  134. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.mts +78 -0
  135. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.mts.map +1 -0
  136. package/dist/data-sources/evm-rpc-services/services/TokenDetector.mjs +226 -0
  137. package/dist/data-sources/evm-rpc-services/services/TokenDetector.mjs.map +1 -0
  138. package/dist/data-sources/evm-rpc-services/services/index.cjs +13 -0
  139. package/dist/data-sources/evm-rpc-services/services/index.cjs.map +1 -0
  140. package/dist/data-sources/evm-rpc-services/services/{index.d.ts → index.d.cts} +4 -4
  141. package/dist/data-sources/evm-rpc-services/services/index.d.cts.map +1 -0
  142. package/dist/data-sources/evm-rpc-services/services/index.d.mts +4 -0
  143. package/dist/data-sources/evm-rpc-services/services/index.d.mts.map +1 -0
  144. package/dist/data-sources/evm-rpc-services/services/index.mjs +4 -0
  145. package/dist/data-sources/evm-rpc-services/services/index.mjs.map +1 -0
  146. package/dist/data-sources/evm-rpc-services/types/assets.cjs +3 -0
  147. package/dist/data-sources/evm-rpc-services/types/assets.cjs.map +1 -0
  148. package/dist/data-sources/evm-rpc-services/types/{assets.d.ts → assets.d.cts} +3 -3
  149. package/dist/data-sources/evm-rpc-services/types/assets.d.cts.map +1 -0
  150. package/dist/data-sources/evm-rpc-services/types/assets.d.mts +53 -0
  151. package/dist/data-sources/evm-rpc-services/types/assets.d.mts.map +1 -0
  152. package/dist/data-sources/evm-rpc-services/types/assets.mjs +2 -0
  153. package/dist/data-sources/evm-rpc-services/types/assets.mjs.map +1 -0
  154. package/dist/data-sources/evm-rpc-services/types/config.cjs +3 -0
  155. package/dist/data-sources/evm-rpc-services/types/config.cjs.map +1 -0
  156. package/dist/data-sources/evm-rpc-services/types/{config.d.ts → config.d.cts} +2 -2
  157. package/dist/data-sources/evm-rpc-services/types/config.d.cts.map +1 -0
  158. package/dist/data-sources/evm-rpc-services/types/config.d.mts +18 -0
  159. package/dist/data-sources/evm-rpc-services/types/config.d.mts.map +1 -0
  160. package/dist/data-sources/evm-rpc-services/types/config.mjs +2 -0
  161. package/dist/data-sources/evm-rpc-services/types/config.mjs.map +1 -0
  162. package/dist/data-sources/evm-rpc-services/types/core.cjs +3 -0
  163. package/dist/data-sources/evm-rpc-services/types/core.cjs.map +1 -0
  164. package/dist/data-sources/evm-rpc-services/types/{core.d.ts → core.d.cts} +2 -2
  165. package/dist/data-sources/evm-rpc-services/types/core.d.cts.map +1 -0
  166. package/dist/data-sources/evm-rpc-services/types/core.d.mts +19 -0
  167. package/dist/data-sources/evm-rpc-services/types/core.d.mts.map +1 -0
  168. package/dist/data-sources/evm-rpc-services/types/core.mjs +2 -0
  169. package/dist/data-sources/evm-rpc-services/types/core.mjs.map +1 -0
  170. package/dist/data-sources/evm-rpc-services/types/index.cjs +3 -0
  171. package/dist/data-sources/evm-rpc-services/types/index.cjs.map +1 -0
  172. package/dist/data-sources/evm-rpc-services/types/index.d.cts +7 -0
  173. package/dist/data-sources/evm-rpc-services/types/index.d.cts.map +1 -0
  174. package/dist/data-sources/evm-rpc-services/types/index.d.mts +7 -0
  175. package/dist/data-sources/evm-rpc-services/types/index.d.mts.map +1 -0
  176. package/dist/data-sources/evm-rpc-services/types/index.mjs +2 -0
  177. package/dist/data-sources/evm-rpc-services/types/index.mjs.map +1 -0
  178. package/dist/data-sources/evm-rpc-services/types/multicall.cjs +3 -0
  179. package/dist/data-sources/evm-rpc-services/types/multicall.cjs.map +1 -0
  180. package/dist/data-sources/evm-rpc-services/types/{multicall.d.ts → multicall.d.cts} +2 -2
  181. package/dist/data-sources/evm-rpc-services/types/multicall.d.cts.map +1 -0
  182. package/dist/data-sources/evm-rpc-services/types/multicall.d.mts +24 -0
  183. package/dist/data-sources/evm-rpc-services/types/multicall.d.mts.map +1 -0
  184. package/dist/data-sources/evm-rpc-services/types/multicall.mjs +2 -0
  185. package/dist/data-sources/evm-rpc-services/types/multicall.mjs.map +1 -0
  186. package/dist/data-sources/evm-rpc-services/types/services.cjs +3 -0
  187. package/dist/data-sources/evm-rpc-services/types/services.cjs.map +1 -0
  188. package/dist/data-sources/evm-rpc-services/types/{services.d.ts → services.d.cts} +3 -3
  189. package/dist/data-sources/evm-rpc-services/types/services.d.cts.map +1 -0
  190. package/dist/data-sources/evm-rpc-services/types/services.d.mts +70 -0
  191. package/dist/data-sources/evm-rpc-services/types/services.d.mts.map +1 -0
  192. package/dist/data-sources/evm-rpc-services/types/services.mjs +2 -0
  193. package/dist/data-sources/evm-rpc-services/types/services.mjs.map +1 -0
  194. package/dist/data-sources/evm-rpc-services/types/state.cjs +3 -0
  195. package/dist/data-sources/evm-rpc-services/types/state.cjs.map +1 -0
  196. package/dist/data-sources/evm-rpc-services/types/{state.d.ts → state.d.cts} +2 -2
  197. package/dist/data-sources/evm-rpc-services/types/state.d.cts.map +1 -0
  198. package/dist/data-sources/evm-rpc-services/types/state.d.mts +58 -0
  199. package/dist/data-sources/evm-rpc-services/types/state.d.mts.map +1 -0
  200. package/dist/data-sources/evm-rpc-services/types/state.mjs +2 -0
  201. package/dist/data-sources/evm-rpc-services/types/state.mjs.map +1 -0
  202. package/dist/data-sources/evm-rpc-services/utils/assets.cjs +9 -0
  203. package/dist/data-sources/evm-rpc-services/utils/assets.cjs.map +1 -0
  204. package/dist/data-sources/evm-rpc-services/utils/{assets.d.ts → assets.d.cts} +2 -2
  205. package/dist/data-sources/evm-rpc-services/utils/assets.d.cts.map +1 -0
  206. package/dist/data-sources/evm-rpc-services/utils/assets.d.mts +3 -0
  207. package/dist/data-sources/evm-rpc-services/utils/assets.d.mts.map +1 -0
  208. package/dist/data-sources/evm-rpc-services/utils/{assets.js → assets.mjs} +2 -2
  209. package/dist/data-sources/evm-rpc-services/utils/assets.mjs.map +1 -0
  210. package/dist/data-sources/evm-rpc-services/utils/batch.cjs +45 -0
  211. package/dist/data-sources/evm-rpc-services/utils/batch.cjs.map +1 -0
  212. package/dist/data-sources/evm-rpc-services/utils/{batch.d.ts → batch.d.cts} +1 -1
  213. package/dist/data-sources/evm-rpc-services/utils/batch.d.cts.map +1 -0
  214. package/dist/data-sources/evm-rpc-services/utils/batch.d.mts +28 -0
  215. package/dist/data-sources/evm-rpc-services/utils/batch.d.mts.map +1 -0
  216. package/dist/data-sources/evm-rpc-services/utils/{batch.js → batch.mjs} +1 -1
  217. package/dist/data-sources/evm-rpc-services/utils/batch.mjs.map +1 -0
  218. package/dist/data-sources/evm-rpc-services/utils/index.cjs +15 -0
  219. package/dist/data-sources/evm-rpc-services/utils/index.cjs.map +1 -0
  220. package/dist/data-sources/evm-rpc-services/utils/index.d.cts +4 -0
  221. package/dist/data-sources/evm-rpc-services/utils/index.d.cts.map +1 -0
  222. package/dist/data-sources/evm-rpc-services/utils/index.d.mts +4 -0
  223. package/dist/data-sources/evm-rpc-services/utils/index.d.mts.map +1 -0
  224. package/dist/data-sources/evm-rpc-services/utils/index.mjs +4 -0
  225. package/dist/data-sources/evm-rpc-services/utils/index.mjs.map +1 -0
  226. package/dist/data-sources/evm-rpc-services/utils/parsing.cjs +47 -0
  227. package/dist/data-sources/evm-rpc-services/utils/parsing.cjs.map +1 -0
  228. package/dist/data-sources/evm-rpc-services/utils/{parsing.d.ts → parsing.d.cts} +3 -3
  229. package/dist/data-sources/evm-rpc-services/utils/parsing.d.cts.map +1 -0
  230. package/dist/data-sources/evm-rpc-services/utils/parsing.d.mts +25 -0
  231. package/dist/data-sources/evm-rpc-services/utils/parsing.d.mts.map +1 -0
  232. package/dist/data-sources/evm-rpc-services/utils/{parsing.js → parsing.mjs} +3 -3
  233. package/dist/data-sources/evm-rpc-services/utils/parsing.mjs.map +1 -0
  234. package/dist/data-sources/evm-rpc-services/utils/staking-contracts.cjs +68 -0
  235. package/dist/data-sources/evm-rpc-services/utils/staking-contracts.cjs.map +1 -0
  236. package/dist/data-sources/evm-rpc-services/utils/{staking-contracts.d.ts → staking-contracts.d.cts} +1 -1
  237. package/dist/data-sources/evm-rpc-services/utils/staking-contracts.d.cts.map +1 -0
  238. package/dist/data-sources/evm-rpc-services/utils/staking-contracts.d.mts +25 -0
  239. package/dist/data-sources/evm-rpc-services/utils/staking-contracts.d.mts.map +1 -0
  240. package/dist/data-sources/evm-rpc-services/utils/{staking-contracts.js → staking-contracts.mjs} +3 -3
  241. package/dist/data-sources/evm-rpc-services/utils/staking-contracts.mjs.map +1 -0
  242. package/dist/data-sources/index.cjs +30 -0
  243. package/dist/data-sources/index.cjs.map +1 -0
  244. package/dist/data-sources/{index.d.ts → index.d.cts} +9 -9
  245. package/dist/data-sources/index.d.cts.map +1 -0
  246. package/dist/data-sources/index.d.mts +9 -0
  247. package/dist/data-sources/index.d.mts.map +1 -0
  248. package/dist/data-sources/index.mjs +14 -0
  249. package/dist/data-sources/index.mjs.map +1 -0
  250. package/dist/defaults.cjs +125 -0
  251. package/dist/defaults.cjs.map +1 -0
  252. package/dist/{defaults.d.ts → defaults.d.cts} +2 -2
  253. package/dist/defaults.d.cts.map +1 -0
  254. package/dist/defaults.d.mts +50 -0
  255. package/dist/defaults.d.mts.map +1 -0
  256. package/dist/{defaults.js → defaults.mjs} +1 -1
  257. package/dist/defaults.mjs.map +1 -0
  258. package/dist/errors.cjs +25 -0
  259. package/dist/errors.cjs.map +1 -0
  260. package/dist/{errors.d.ts → errors.d.cts} +1 -1
  261. package/dist/errors.d.cts.map +1 -0
  262. package/dist/errors.d.mts +25 -0
  263. package/dist/errors.d.mts.map +1 -0
  264. package/dist/{errors.js → errors.mjs} +1 -7
  265. package/dist/errors.mjs.map +1 -0
  266. package/dist/index.cjs +67 -0
  267. package/dist/index.cjs.map +1 -0
  268. package/dist/{index.d.ts → index.d.cts} +26 -26
  269. package/dist/index.d.cts.map +1 -0
  270. package/dist/index.d.mts +26 -0
  271. package/dist/index.d.mts.map +1 -0
  272. package/dist/{index.js → index.mjs} +13 -13
  273. package/dist/index.mjs.map +1 -0
  274. package/dist/logger.cjs +7 -0
  275. package/dist/logger.cjs.map +1 -0
  276. package/dist/logger.d.cts +5 -0
  277. package/dist/logger.d.cts.map +1 -0
  278. package/dist/logger.d.mts +5 -0
  279. package/dist/logger.d.mts.map +1 -0
  280. package/dist/logger.mjs +4 -0
  281. package/dist/logger.mjs.map +1 -0
  282. package/dist/middlewares/CustomAssetGraduationMiddleware.cjs +132 -0
  283. package/dist/middlewares/CustomAssetGraduationMiddleware.cjs.map +1 -0
  284. package/dist/middlewares/{CustomAssetGraduationMiddleware.d.ts → CustomAssetGraduationMiddleware.d.cts} +2 -2
  285. package/dist/middlewares/CustomAssetGraduationMiddleware.d.cts.map +1 -0
  286. package/dist/middlewares/CustomAssetGraduationMiddleware.d.mts +32 -0
  287. package/dist/middlewares/CustomAssetGraduationMiddleware.d.mts.map +1 -0
  288. package/dist/middlewares/{CustomAssetGraduationMiddleware.js → CustomAssetGraduationMiddleware.mjs} +25 -12
  289. package/dist/middlewares/CustomAssetGraduationMiddleware.mjs.map +1 -0
  290. package/dist/middlewares/DetectionMiddleware.cjs +154 -0
  291. package/dist/middlewares/DetectionMiddleware.cjs.map +1 -0
  292. package/dist/middlewares/{DetectionMiddleware.d.ts → DetectionMiddleware.d.cts} +2 -2
  293. package/dist/middlewares/DetectionMiddleware.d.cts.map +1 -0
  294. package/dist/middlewares/DetectionMiddleware.d.mts +40 -0
  295. package/dist/middlewares/DetectionMiddleware.d.mts.map +1 -0
  296. package/dist/middlewares/{DetectionMiddleware.js → DetectionMiddleware.mjs} +7 -5
  297. package/dist/middlewares/DetectionMiddleware.mjs.map +1 -0
  298. package/dist/middlewares/ParallelMiddleware.cjs +263 -0
  299. package/dist/middlewares/ParallelMiddleware.cjs.map +1 -0
  300. package/dist/middlewares/{ParallelMiddleware.d.ts → ParallelMiddleware.d.cts} +2 -2
  301. package/dist/middlewares/ParallelMiddleware.d.cts.map +1 -0
  302. package/dist/middlewares/ParallelMiddleware.d.mts +41 -0
  303. package/dist/middlewares/ParallelMiddleware.d.mts.map +1 -0
  304. package/dist/middlewares/{ParallelMiddleware.js → ParallelMiddleware.mjs} +11 -4
  305. package/dist/middlewares/ParallelMiddleware.mjs.map +1 -0
  306. package/dist/middlewares/RpcFallbackMiddleware.cjs +90 -0
  307. package/dist/middlewares/RpcFallbackMiddleware.cjs.map +1 -0
  308. package/dist/middlewares/{RpcFallbackMiddleware.d.ts → RpcFallbackMiddleware.d.cts} +2 -2
  309. package/dist/middlewares/RpcFallbackMiddleware.d.cts.map +1 -0
  310. package/dist/middlewares/RpcFallbackMiddleware.d.mts +23 -0
  311. package/dist/middlewares/RpcFallbackMiddleware.d.mts.map +1 -0
  312. package/dist/middlewares/{RpcFallbackMiddleware.js → RpcFallbackMiddleware.mjs} +21 -8
  313. package/dist/middlewares/RpcFallbackMiddleware.mjs.map +1 -0
  314. package/dist/middlewares/index.cjs +14 -0
  315. package/dist/middlewares/index.cjs.map +1 -0
  316. package/dist/middlewares/index.d.cts +9 -0
  317. package/dist/middlewares/index.d.cts.map +1 -0
  318. package/dist/middlewares/index.d.mts +9 -0
  319. package/dist/middlewares/index.d.mts.map +1 -0
  320. package/dist/middlewares/index.mjs +5 -0
  321. package/dist/middlewares/index.mjs.map +1 -0
  322. package/dist/migrations/healAssetsInfoMetadata.cjs +539 -0
  323. package/dist/migrations/healAssetsInfoMetadata.cjs.map +1 -0
  324. package/dist/migrations/{healAssetsInfoMetadata.d.ts → healAssetsInfoMetadata.d.cts} +5 -5
  325. package/dist/migrations/healAssetsInfoMetadata.d.cts.map +1 -0
  326. package/dist/migrations/healAssetsInfoMetadata.d.mts +140 -0
  327. package/dist/migrations/healAssetsInfoMetadata.d.mts.map +1 -0
  328. package/dist/migrations/{healAssetsInfoMetadata.js → healAssetsInfoMetadata.mjs} +11 -9
  329. package/dist/migrations/healAssetsInfoMetadata.mjs.map +1 -0
  330. package/dist/selectors/balance.cjs +532 -0
  331. package/dist/selectors/balance.cjs.map +1 -0
  332. package/dist/selectors/{balance.d.ts → balance.d.cts} +6 -6
  333. package/dist/selectors/balance.d.cts.map +1 -0
  334. package/dist/selectors/balance.d.mts +103 -0
  335. package/dist/selectors/balance.d.mts.map +1 -0
  336. package/dist/selectors/{balance.js → balance.mjs} +5 -5
  337. package/dist/selectors/balance.mjs.map +1 -0
  338. package/dist/types.cjs +39 -0
  339. package/dist/types.cjs.map +1 -0
  340. package/dist/{types.d.ts → types.d.cts} +4 -4
  341. package/dist/types.d.cts.map +1 -0
  342. package/dist/types.d.mts +533 -0
  343. package/dist/types.d.mts.map +1 -0
  344. package/dist/{types.js → types.mjs} +1 -1
  345. package/dist/types.mjs.map +1 -0
  346. package/dist/utils/constants.cjs +5 -0
  347. package/dist/utils/constants.cjs.map +1 -0
  348. package/dist/utils/constants.d.cts +2 -0
  349. package/dist/utils/constants.d.cts.map +1 -0
  350. package/dist/utils/constants.d.mts +2 -0
  351. package/dist/utils/constants.d.mts.map +1 -0
  352. package/dist/utils/{constants.js → constants.mjs} +1 -1
  353. package/dist/utils/constants.mjs.map +1 -0
  354. package/dist/utils/customAssetsRpcSupplement.cjs +72 -0
  355. package/dist/utils/customAssetsRpcSupplement.cjs.map +1 -0
  356. package/dist/utils/{customAssetsRpcSupplement.d.ts → customAssetsRpcSupplement.d.cts} +2 -2
  357. package/dist/utils/customAssetsRpcSupplement.d.cts.map +1 -0
  358. package/dist/utils/customAssetsRpcSupplement.d.mts +60 -0
  359. package/dist/utils/customAssetsRpcSupplement.d.mts.map +1 -0
  360. package/dist/utils/{customAssetsRpcSupplement.js → customAssetsRpcSupplement.mjs} +2 -2
  361. package/dist/utils/customAssetsRpcSupplement.mjs.map +1 -0
  362. package/dist/utils/dedupingBatchFetcher.cjs +168 -0
  363. package/dist/utils/dedupingBatchFetcher.cjs.map +1 -0
  364. package/dist/utils/{dedupingBatchFetcher.d.ts → dedupingBatchFetcher.d.cts} +1 -1
  365. package/dist/utils/dedupingBatchFetcher.d.cts.map +1 -0
  366. package/dist/utils/dedupingBatchFetcher.d.mts +72 -0
  367. package/dist/utils/dedupingBatchFetcher.d.mts.map +1 -0
  368. package/dist/utils/dedupingBatchFetcher.mjs +164 -0
  369. package/dist/utils/dedupingBatchFetcher.mjs.map +1 -0
  370. package/dist/utils/fetchWithTimeout.cjs +30 -0
  371. package/dist/utils/fetchWithTimeout.cjs.map +1 -0
  372. package/dist/utils/{fetchWithTimeout.d.ts → fetchWithTimeout.d.cts} +1 -1
  373. package/dist/utils/fetchWithTimeout.d.cts.map +1 -0
  374. package/dist/utils/fetchWithTimeout.d.mts +11 -0
  375. package/dist/utils/fetchWithTimeout.d.mts.map +1 -0
  376. package/dist/utils/{fetchWithTimeout.js → fetchWithTimeout.mjs} +1 -1
  377. package/dist/utils/fetchWithTimeout.mjs.map +1 -0
  378. package/dist/utils/formatExchangeRatesForBridge.cjs +161 -0
  379. package/dist/utils/formatExchangeRatesForBridge.cjs.map +1 -0
  380. package/dist/utils/{formatExchangeRatesForBridge.d.ts → formatExchangeRatesForBridge.d.cts} +3 -3
  381. package/dist/utils/formatExchangeRatesForBridge.d.cts.map +1 -0
  382. package/dist/utils/formatExchangeRatesForBridge.d.mts +43 -0
  383. package/dist/utils/formatExchangeRatesForBridge.d.mts.map +1 -0
  384. package/dist/utils/{formatExchangeRatesForBridge.js → formatExchangeRatesForBridge.mjs} +7 -6
  385. package/dist/utils/formatExchangeRatesForBridge.mjs.map +1 -0
  386. package/dist/utils/formatStateForTransactionPay.cjs +174 -0
  387. package/dist/utils/formatStateForTransactionPay.cjs.map +1 -0
  388. package/dist/utils/{formatStateForTransactionPay.d.ts → formatStateForTransactionPay.d.cts} +3 -3
  389. package/dist/utils/formatStateForTransactionPay.d.cts.map +1 -0
  390. package/dist/utils/formatStateForTransactionPay.d.mts +72 -0
  391. package/dist/utils/formatStateForTransactionPay.d.mts.map +1 -0
  392. package/dist/utils/{formatStateForTransactionPay.js → formatStateForTransactionPay.mjs} +15 -13
  393. package/dist/utils/formatStateForTransactionPay.mjs.map +1 -0
  394. package/dist/utils/index.cjs +21 -0
  395. package/dist/utils/index.cjs.map +1 -0
  396. package/dist/utils/index.d.cts +9 -0
  397. package/dist/utils/index.d.cts.map +1 -0
  398. package/dist/utils/index.d.mts +9 -0
  399. package/dist/utils/index.d.mts.map +1 -0
  400. package/dist/utils/index.mjs +7 -0
  401. package/dist/utils/index.mjs.map +1 -0
  402. package/dist/utils/native-assets.cjs +64 -0
  403. package/dist/utils/native-assets.cjs.map +1 -0
  404. package/dist/utils/{native-assets.d.ts → native-assets.d.cts} +2 -2
  405. package/dist/utils/native-assets.d.cts.map +1 -0
  406. package/dist/utils/native-assets.d.mts +19 -0
  407. package/dist/utils/native-assets.d.mts.map +1 -0
  408. package/dist/utils/{native-assets.js → native-assets.mjs} +6 -6
  409. package/dist/utils/native-assets.mjs.map +1 -0
  410. package/dist/utils/normalizeAmountString.cjs +64 -0
  411. package/dist/utils/normalizeAmountString.cjs.map +1 -0
  412. package/dist/utils/{normalizeAmountString.d.ts → normalizeAmountString.d.cts} +1 -1
  413. package/dist/utils/normalizeAmountString.d.cts.map +1 -0
  414. package/dist/utils/normalizeAmountString.d.mts +35 -0
  415. package/dist/utils/normalizeAmountString.d.mts.map +1 -0
  416. package/dist/utils/{normalizeAmountString.js → normalizeAmountString.mjs} +2 -2
  417. package/dist/utils/normalizeAmountString.mjs.map +1 -0
  418. package/dist/utils/normalizeAssetId.cjs +60 -0
  419. package/dist/utils/normalizeAssetId.cjs.map +1 -0
  420. package/dist/utils/{normalizeAssetId.d.ts → normalizeAssetId.d.cts} +3 -3
  421. package/dist/utils/normalizeAssetId.d.cts.map +1 -0
  422. package/dist/utils/normalizeAssetId.d.mts +32 -0
  423. package/dist/utils/normalizeAssetId.d.mts.map +1 -0
  424. package/dist/utils/{normalizeAssetId.js → normalizeAssetId.mjs} +11 -4
  425. package/dist/utils/normalizeAssetId.mjs.map +1 -0
  426. package/dist/utils/snaps-assets-migration.cjs +171 -0
  427. package/dist/utils/snaps-assets-migration.cjs.map +1 -0
  428. package/dist/utils/{snaps-assets-migration.d.ts → snaps-assets-migration.d.cts} +5 -5
  429. package/dist/utils/snaps-assets-migration.d.cts.map +1 -0
  430. package/dist/utils/snaps-assets-migration.d.mts +113 -0
  431. package/dist/utils/snaps-assets-migration.d.mts.map +1 -0
  432. package/dist/utils/{snaps-assets-migration.js → snaps-assets-migration.mjs} +2 -2
  433. package/dist/utils/snaps-assets-migration.mjs.map +1 -0
  434. package/dist/utils/trace.cjs +120 -0
  435. package/dist/utils/trace.cjs.map +1 -0
  436. package/dist/utils/{trace.d.ts → trace.d.cts} +2 -2
  437. package/dist/utils/trace.d.cts.map +1 -0
  438. package/dist/utils/trace.d.mts +52 -0
  439. package/dist/utils/trace.d.mts.map +1 -0
  440. package/dist/utils/{trace.js → trace.mjs} +1 -1
  441. package/dist/utils/trace.mjs.map +1 -0
  442. package/package.json +17 -14
  443. package/dist/AssetsController-method-action-types.d.ts.map +0 -1
  444. package/dist/AssetsController-method-action-types.js.map +0 -1
  445. package/dist/AssetsController.d.ts.map +0 -1
  446. package/dist/AssetsController.js +0 -2934
  447. package/dist/AssetsController.js.map +0 -1
  448. package/dist/data-sources/AbstractDataSource.d.ts.map +0 -1
  449. package/dist/data-sources/AbstractDataSource.js.map +0 -1
  450. package/dist/data-sources/AccountActivityDataSource.d.ts.map +0 -1
  451. package/dist/data-sources/AccountActivityDataSource.js +0 -272
  452. package/dist/data-sources/AccountActivityDataSource.js.map +0 -1
  453. package/dist/data-sources/AccountsApiDataSource.d.ts.map +0 -1
  454. package/dist/data-sources/AccountsApiDataSource.js +0 -609
  455. package/dist/data-sources/AccountsApiDataSource.js.map +0 -1
  456. package/dist/data-sources/PriceDataSource.d.ts.map +0 -1
  457. package/dist/data-sources/PriceDataSource.js.map +0 -1
  458. package/dist/data-sources/RpcDataSource.d.ts.map +0 -1
  459. package/dist/data-sources/RpcDataSource.js +0 -1077
  460. package/dist/data-sources/RpcDataSource.js.map +0 -1
  461. package/dist/data-sources/SnapDataSource.d.ts.map +0 -1
  462. package/dist/data-sources/SnapDataSource.js.map +0 -1
  463. package/dist/data-sources/StakedBalanceDataSource.d.ts.map +0 -1
  464. package/dist/data-sources/StakedBalanceDataSource.js +0 -675
  465. package/dist/data-sources/StakedBalanceDataSource.js.map +0 -1
  466. package/dist/data-sources/TokenDataSource.d.ts.map +0 -1
  467. package/dist/data-sources/TokenDataSource.js.map +0 -1
  468. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.d.ts.map +0 -1
  469. package/dist/data-sources/evm-rpc-services/clients/MulticallClient.js.map +0 -1
  470. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.d.ts.map +0 -1
  471. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.js +0 -297
  472. package/dist/data-sources/evm-rpc-services/clients/TokensApiClient.js.map +0 -1
  473. package/dist/data-sources/evm-rpc-services/clients/index.d.ts +0 -4
  474. package/dist/data-sources/evm-rpc-services/clients/index.d.ts.map +0 -1
  475. package/dist/data-sources/evm-rpc-services/clients/index.js +0 -3
  476. package/dist/data-sources/evm-rpc-services/clients/index.js.map +0 -1
  477. package/dist/data-sources/evm-rpc-services/index.d.ts.map +0 -1
  478. package/dist/data-sources/evm-rpc-services/index.js +0 -4
  479. package/dist/data-sources/evm-rpc-services/index.js.map +0 -1
  480. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.d.ts.map +0 -1
  481. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.js +0 -241
  482. package/dist/data-sources/evm-rpc-services/services/BalanceFetcher.js.map +0 -1
  483. package/dist/data-sources/evm-rpc-services/services/StakedBalanceFetcher.d.ts.map +0 -1
  484. package/dist/data-sources/evm-rpc-services/services/StakedBalanceFetcher.js.map +0 -1
  485. package/dist/data-sources/evm-rpc-services/services/TokenDetector.d.ts.map +0 -1
  486. package/dist/data-sources/evm-rpc-services/services/TokenDetector.js +0 -217
  487. package/dist/data-sources/evm-rpc-services/services/TokenDetector.js.map +0 -1
  488. package/dist/data-sources/evm-rpc-services/services/index.d.ts.map +0 -1
  489. package/dist/data-sources/evm-rpc-services/services/index.js +0 -4
  490. package/dist/data-sources/evm-rpc-services/services/index.js.map +0 -1
  491. package/dist/data-sources/evm-rpc-services/types/assets.d.ts.map +0 -1
  492. package/dist/data-sources/evm-rpc-services/types/assets.js +0 -2
  493. package/dist/data-sources/evm-rpc-services/types/assets.js.map +0 -1
  494. package/dist/data-sources/evm-rpc-services/types/config.d.ts.map +0 -1
  495. package/dist/data-sources/evm-rpc-services/types/config.js +0 -2
  496. package/dist/data-sources/evm-rpc-services/types/config.js.map +0 -1
  497. package/dist/data-sources/evm-rpc-services/types/core.d.ts.map +0 -1
  498. package/dist/data-sources/evm-rpc-services/types/core.js +0 -2
  499. package/dist/data-sources/evm-rpc-services/types/core.js.map +0 -1
  500. package/dist/data-sources/evm-rpc-services/types/index.d.ts +0 -7
  501. package/dist/data-sources/evm-rpc-services/types/index.d.ts.map +0 -1
  502. package/dist/data-sources/evm-rpc-services/types/index.js +0 -2
  503. package/dist/data-sources/evm-rpc-services/types/index.js.map +0 -1
  504. package/dist/data-sources/evm-rpc-services/types/multicall.d.ts.map +0 -1
  505. package/dist/data-sources/evm-rpc-services/types/multicall.js +0 -2
  506. package/dist/data-sources/evm-rpc-services/types/multicall.js.map +0 -1
  507. package/dist/data-sources/evm-rpc-services/types/services.d.ts.map +0 -1
  508. package/dist/data-sources/evm-rpc-services/types/services.js +0 -2
  509. package/dist/data-sources/evm-rpc-services/types/services.js.map +0 -1
  510. package/dist/data-sources/evm-rpc-services/types/state.d.ts.map +0 -1
  511. package/dist/data-sources/evm-rpc-services/types/state.js +0 -2
  512. package/dist/data-sources/evm-rpc-services/types/state.js.map +0 -1
  513. package/dist/data-sources/evm-rpc-services/utils/assets.d.ts.map +0 -1
  514. package/dist/data-sources/evm-rpc-services/utils/assets.js.map +0 -1
  515. package/dist/data-sources/evm-rpc-services/utils/batch.d.ts.map +0 -1
  516. package/dist/data-sources/evm-rpc-services/utils/batch.js.map +0 -1
  517. package/dist/data-sources/evm-rpc-services/utils/index.d.ts +0 -4
  518. package/dist/data-sources/evm-rpc-services/utils/index.d.ts.map +0 -1
  519. package/dist/data-sources/evm-rpc-services/utils/index.js +0 -4
  520. package/dist/data-sources/evm-rpc-services/utils/index.js.map +0 -1
  521. package/dist/data-sources/evm-rpc-services/utils/parsing.d.ts.map +0 -1
  522. package/dist/data-sources/evm-rpc-services/utils/parsing.js.map +0 -1
  523. package/dist/data-sources/evm-rpc-services/utils/staking-contracts.d.ts.map +0 -1
  524. package/dist/data-sources/evm-rpc-services/utils/staking-contracts.js.map +0 -1
  525. package/dist/data-sources/index.d.ts.map +0 -1
  526. package/dist/data-sources/index.js +0 -14
  527. package/dist/data-sources/index.js.map +0 -1
  528. package/dist/defaults.d.ts.map +0 -1
  529. package/dist/defaults.js.map +0 -1
  530. package/dist/errors.d.ts.map +0 -1
  531. package/dist/errors.js.map +0 -1
  532. package/dist/index.d.ts.map +0 -1
  533. package/dist/index.js.map +0 -1
  534. package/dist/logger.d.ts +0 -4
  535. package/dist/logger.d.ts.map +0 -1
  536. package/dist/logger.js +0 -4
  537. package/dist/logger.js.map +0 -1
  538. package/dist/middlewares/CustomAssetGraduationMiddleware.d.ts.map +0 -1
  539. package/dist/middlewares/CustomAssetGraduationMiddleware.js.map +0 -1
  540. package/dist/middlewares/DetectionMiddleware.d.ts.map +0 -1
  541. package/dist/middlewares/DetectionMiddleware.js.map +0 -1
  542. package/dist/middlewares/ParallelMiddleware.d.ts.map +0 -1
  543. package/dist/middlewares/ParallelMiddleware.js.map +0 -1
  544. package/dist/middlewares/RpcFallbackMiddleware.d.ts.map +0 -1
  545. package/dist/middlewares/RpcFallbackMiddleware.js.map +0 -1
  546. package/dist/middlewares/index.d.ts +0 -9
  547. package/dist/middlewares/index.d.ts.map +0 -1
  548. package/dist/middlewares/index.js +0 -5
  549. package/dist/middlewares/index.js.map +0 -1
  550. package/dist/migrations/healAssetsInfoMetadata.d.ts.map +0 -1
  551. package/dist/migrations/healAssetsInfoMetadata.js.map +0 -1
  552. package/dist/selectors/balance.d.ts.map +0 -1
  553. package/dist/selectors/balance.js.map +0 -1
  554. package/dist/types.d.ts.map +0 -1
  555. package/dist/types.js.map +0 -1
  556. package/dist/utils/constants.d.ts +0 -2
  557. package/dist/utils/constants.d.ts.map +0 -1
  558. package/dist/utils/constants.js.map +0 -1
  559. package/dist/utils/customAssetsRpcSupplement.d.ts.map +0 -1
  560. package/dist/utils/customAssetsRpcSupplement.js.map +0 -1
  561. package/dist/utils/dedupingBatchFetcher.d.ts.map +0 -1
  562. package/dist/utils/dedupingBatchFetcher.js +0 -177
  563. package/dist/utils/dedupingBatchFetcher.js.map +0 -1
  564. package/dist/utils/fetchWithTimeout.d.ts.map +0 -1
  565. package/dist/utils/fetchWithTimeout.js.map +0 -1
  566. package/dist/utils/formatExchangeRatesForBridge.d.ts.map +0 -1
  567. package/dist/utils/formatExchangeRatesForBridge.js.map +0 -1
  568. package/dist/utils/formatStateForTransactionPay.d.ts.map +0 -1
  569. package/dist/utils/formatStateForTransactionPay.js.map +0 -1
  570. package/dist/utils/index.d.ts +0 -9
  571. package/dist/utils/index.d.ts.map +0 -1
  572. package/dist/utils/index.js +0 -7
  573. package/dist/utils/index.js.map +0 -1
  574. package/dist/utils/native-assets.d.ts.map +0 -1
  575. package/dist/utils/native-assets.js.map +0 -1
  576. package/dist/utils/normalizeAmountString.d.ts.map +0 -1
  577. package/dist/utils/normalizeAmountString.js.map +0 -1
  578. package/dist/utils/normalizeAssetId.d.ts.map +0 -1
  579. package/dist/utils/normalizeAssetId.js.map +0 -1
  580. package/dist/utils/snaps-assets-migration.d.ts.map +0 -1
  581. package/dist/utils/snaps-assets-migration.js.map +0 -1
  582. package/dist/utils/trace.d.ts.map +0 -1
  583. package/dist/utils/trace.js.map +0 -1
@@ -1,2934 +0,0 @@
1
- import { BaseController } from '@metamask/base-controller';
2
- import { clientControllerSelectors } from '@metamask/client-controller';
3
- import { isCaipChainId, isStrictHexString, KnownCaipNamespace, parseCaipAssetType, parseCaipChainId, } from '@metamask/utils';
4
- import { Mutex } from 'async-mutex';
5
- import { BigNumber as BigNumberJS } from 'bignumber.js';
6
- import { isEqual } from 'lodash-es';
7
- import { AccountActivityDataSource } from './data-sources/AccountActivityDataSource.js';
8
- import { AccountsApiDataSource } from './data-sources/AccountsApiDataSource.js';
9
- import { isStakingContractAssetId } from './data-sources/evm-rpc-services/index.js';
10
- import { shouldSkipNativeForCaipChainId } from './data-sources/evm-rpc-services/utils/assets.js';
11
- import { isPriceableAsset, PriceDataSource, } from './data-sources/PriceDataSource.js';
12
- import { RpcDataSource } from './data-sources/RpcDataSource.js';
13
- import { SnapDataSource } from './data-sources/SnapDataSource.js';
14
- import { StakedBalanceDataSource } from './data-sources/StakedBalanceDataSource.js';
15
- import { CaipAssetNamespace, TokenDataSource, } from './data-sources/TokenDataSource.js';
16
- import { CHAINS_WITH_DEFAULT_TRACKED_ASSETS, DEFAULT_TRACKED_ASSETS_BY_CHAIN, buildDefaultAssetsInfo, getDefaultAssetMetadata, } from './defaults.js';
17
- import { AssetsDataSourceError } from './errors.js';
18
- import { projectLogger, createModuleLogger } from './logger.js';
19
- import { CustomAssetGraduationMiddleware } from './middlewares/CustomAssetGraduationMiddleware.js';
20
- import { DetectionMiddleware } from './middlewares/DetectionMiddleware.js';
21
- import { createParallelBalanceMiddleware, createParallelMiddleware, } from './middlewares/ParallelMiddleware.js';
22
- import { RpcFallbackMiddleware } from './middlewares/RpcFallbackMiddleware.js';
23
- import { cleanSpamAssets, isUnlockCleanupEnabled, tempHealAssetsInfoMetadata, } from './migrations/healAssetsInfoMetadata.js';
24
- import { ZERO_ADDRESS } from './utils/constants.js';
25
- import { pickRpcCustomAssetsSupplement } from './utils/customAssetsRpcSupplement.js';
26
- import { normalizeAmountString, normalizeAssetId, formatExchangeRatesForBridge, formatStateForTransactionPay, buildNativeAssetsFromConstant, buildNativeAssetsFromApi, } from './utils/index.js';
27
- import { emitTrace, withTrace } from './utils/trace.js';
28
- const NATIVE_ASSETS_QUERY_KEY = ['nativeAssets'];
29
- /** Stable empty map so memoized formatters are not busted by `?? {}` identity churn. */
30
- const EMPTY_NATIVE_ASSET_MAP = {};
31
- // ============================================================================
32
- // CONTROLLER CONSTANTS
33
- // ============================================================================
34
- const CONTROLLER_NAME = 'AssetsController';
35
- /** Method names exposed as messenger actions (AssetsController:getAssets, etc.) */
36
- const MESSENGER_EXPOSED_METHODS = [
37
- 'getAssets',
38
- 'getAssetsBalance',
39
- 'getAssetMetadata',
40
- 'getAccountAssetByID',
41
- 'getAccountAssetsByIDs',
42
- 'getAccountAssetsByScope',
43
- 'getAssetsPrice',
44
- 'getExchangeRatesForBridge',
45
- 'getStateForTransactionPay',
46
- 'addCustomAsset',
47
- 'removeCustomAsset',
48
- 'getCustomAssets',
49
- 'hideAsset',
50
- 'unhideAsset',
51
- 'setSelectedCurrency',
52
- ];
53
- /** Default polling interval hint for data sources (30 seconds) */
54
- const DEFAULT_POLLING_INTERVAL_MS = 30_000;
55
- // ============================================================================
56
- // TRACE NAMES — used in Sentry spans (search these strings in Discover)
57
- // ============================================================================
58
- const TRACE_FIRST_INIT_FETCH = 'AssetsControllerFirstInitFetch';
59
- const TRACE_FULL_FETCH = 'AssetsFullFetch';
60
- /** Parent span that nests per-source timings; dashboard charts {@link TRACE_FULL_FETCH}. */
61
- const TRACE_FETCH_PIPELINE = 'AssetsFetchPipeline';
62
- const TRACE_BACKGROUND_FETCH = 'AssetsBackgroundFetch';
63
- const TRACE_DATA_SOURCE_TIMING = 'AssetsDataSourceTiming';
64
- const TRACE_DATA_SOURCE_ERROR = 'AssetsDataSourceError';
65
- const TRACE_UPDATE_PIPELINE = 'AssetsUpdatePipeline';
66
- /** Parent span that nests update enrichment; dashboard charts {@link TRACE_UPDATE_PIPELINE}. */
67
- const TRACE_UPDATE_PARENT = 'AssetsUpdateEnrichment';
68
- const TRACE_SUBSCRIPTION_ERROR = 'AssetsSubscriptionError';
69
- const TRACE_STATE_SIZE = 'AssetsStateSize';
70
- const log = createModuleLogger(projectLogger, CONTROLLER_NAME);
71
- /**
72
- * Returns the default state for AssetsController.
73
- *
74
- * @returns The default AssetsController state. Balance, price, and custom
75
- * asset maps start empty; `assetsInfo` is pre-seeded for default tracked
76
- * assets (see {@link buildDefaultAssetsInfo}).
77
- */
78
- export function getDefaultAssetsControllerState() {
79
- return {
80
- // Pre-populate metadata for controller-managed default tracked assets
81
- // (e.g. mUSD on Ethereum mainnet, Linea, Monad) so they are
82
- // immediately renderable in the UI before any RPC poll completes.
83
- // Per-account zero-balance entries are filled into `assetsBalance`
84
- // by `#ensureDefaultTrackedAssetsSeeded` once accounts are known.
85
- assetsInfo: buildDefaultAssetsInfo(),
86
- assetsBalance: {},
87
- assetsPrice: {},
88
- customAssets: {},
89
- assetPreferences: {},
90
- selectedCurrency: 'usd',
91
- };
92
- }
93
- // ============================================================================
94
- // STATE METADATA
95
- // ============================================================================
96
- const stateMetadata = {
97
- assetsInfo: {
98
- persist: true,
99
- includeInStateLogs: false,
100
- includeInDebugSnapshot: false,
101
- usedInUi: true,
102
- },
103
- assetsBalance: {
104
- persist: true,
105
- includeInStateLogs: false,
106
- includeInDebugSnapshot: false,
107
- usedInUi: true,
108
- },
109
- assetsPrice: {
110
- persist: true,
111
- includeInStateLogs: false,
112
- includeInDebugSnapshot: false,
113
- usedInUi: true,
114
- },
115
- customAssets: {
116
- persist: true,
117
- includeInStateLogs: false,
118
- includeInDebugSnapshot: false,
119
- usedInUi: true,
120
- },
121
- assetPreferences: {
122
- persist: true,
123
- includeInStateLogs: false,
124
- includeInDebugSnapshot: false,
125
- usedInUi: true,
126
- },
127
- selectedCurrency: {
128
- persist: true,
129
- includeInStateLogs: false,
130
- includeInDebugSnapshot: false,
131
- usedInUi: true,
132
- },
133
- };
134
- // ============================================================================
135
- // HELPER FUNCTIONS
136
- // ============================================================================
137
- function extractChainId(assetId) {
138
- const parsed = parseCaipAssetType(assetId);
139
- return parsed.chainId;
140
- }
141
- /**
142
- * Normalizes all asset IDs in a DataResponse.
143
- * This is applied at the controller level to ensure consistent state
144
- * regardless of how data sources format their asset IDs.
145
- *
146
- * @param response - The DataResponse to normalize.
147
- * @returns The normalized DataResponse with checksummed EVM addresses.
148
- */
149
- function normalizeResponse(response) {
150
- const normalized = {};
151
- if (response.assetsInfo) {
152
- normalized.assetsInfo = {};
153
- for (const [assetId, metadata] of Object.entries(response.assetsInfo)) {
154
- const normalizedId = normalizeAssetId(assetId);
155
- normalized.assetsInfo[normalizedId] = metadata;
156
- }
157
- }
158
- if (response.assetsPrice) {
159
- normalized.assetsPrice = {};
160
- for (const [assetId, price] of Object.entries(response.assetsPrice)) {
161
- const normalizedId = normalizeAssetId(assetId);
162
- normalized.assetsPrice[normalizedId] = price;
163
- }
164
- }
165
- if (response.assetsBalance) {
166
- normalized.assetsBalance = {};
167
- for (const [accountId, balances] of Object.entries(response.assetsBalance)) {
168
- normalized.assetsBalance[accountId] = {};
169
- for (const [assetId, balance] of Object.entries(balances)) {
170
- const normalizedId = normalizeAssetId(assetId);
171
- normalized.assetsBalance[accountId][normalizedId] = balance;
172
- }
173
- }
174
- }
175
- // Preserve detectedAssets with normalized asset IDs
176
- if (response.detectedAssets) {
177
- normalized.detectedAssets = {};
178
- for (const [accountId, assetIds] of Object.entries(response.detectedAssets)) {
179
- normalized.detectedAssets[accountId] = assetIds.map((assetId) => normalizeAssetId(assetId));
180
- }
181
- }
182
- // Preserve errors (chain IDs don't need normalization)
183
- if (response.errors) {
184
- normalized.errors = { ...response.errors };
185
- }
186
- if (response.updateMode) {
187
- normalized.updateMode = response.updateMode;
188
- }
189
- if (response.replaceCoveredChainBalances) {
190
- normalized.replaceCoveredChainBalances = true;
191
- }
192
- return normalized;
193
- }
194
- /**
195
- * Merge account balances from a data-source response into prior state.
196
- *
197
- * @param previousBalances - Balances already in state for this account.
198
- * @param accountBalances - Balances from the incoming response.
199
- * @param customAssetIds - Custom assets to preserve when replacing covered chains.
200
- * @param replaceCoveredChains - When true, drop prior balances on chains present
201
- * in the response before applying it (authoritative chain slice).
202
- * @returns The merged balance map for the account.
203
- */
204
- function mergeAccountBalances(previousBalances, accountBalances, customAssetIds, replaceCoveredChains) {
205
- if (!replaceCoveredChains) {
206
- return { ...previousBalances, ...accountBalances };
207
- }
208
- const coveredChains = new Set(Object.keys(accountBalances).map((assetId) => assetId.split('/')[0]));
209
- const next = {};
210
- for (const [assetId, balance] of Object.entries(previousBalances)) {
211
- if (!coveredChains.has(assetId.split('/')[0])) {
212
- next[assetId] = balance;
213
- }
214
- }
215
- Object.assign(next, accountBalances);
216
- for (const customId of customAssetIds) {
217
- if (!Object.prototype.hasOwnProperty.call(next, customId)) {
218
- const prev = previousBalances[customId];
219
- next[customId] = prev ?? { amount: '0' };
220
- }
221
- }
222
- // Staked vault balances are owned by StakedBalanceDataSource. When an
223
- // Accounts API (or other) chain-slice replace omits them, keep the prior
224
- // on-chain staked amount instead of clearing it to missing/0.
225
- for (const [assetId, balance] of Object.entries(previousBalances)) {
226
- if (isStakingContractAssetId(assetId) &&
227
- !Object.prototype.hasOwnProperty.call(next, assetId)) {
228
- next[assetId] = balance;
229
- }
230
- }
231
- return next;
232
- }
233
- // ============================================================================
234
- // CONTROLLER IMPLEMENTATION
235
- // ============================================================================
236
- /**
237
- * AssetsController provides a unified interface for managing asset balances
238
- * across all blockchain networks (EVM and non-EVM) and all asset types.
239
- *
240
- * ## Core Responsibilities
241
- *
242
- * 1. **One-Time Fetch (Sync)**: For initial load, force refresh, or on-demand queries.
243
- * Uses `getAssets()`, `getAssetsBalance()`, etc. with `forceUpdate: true`.
244
- *
245
- * 2. **Async Subscriptions**: Subscribes to data sources for ongoing updates.
246
- * Data sources push updates via callbacks; the controller updates state.
247
- *
248
- * 3. **Dynamic Source Selection**: Routes requests to appropriate data sources
249
- * based on which chains they support. When active chains change, the controller
250
- * dynamically adjusts subscriptions.
251
- *
252
- * 4. **Client + Keyring Lifecycle**: Starts subscriptions only when both the UI is
253
- * open (ClientController) and the wallet is unlocked (KeyringController).
254
- * Stops when either the UI closes or the keyring locks. See client-controller
255
- * README for the combined pattern.
256
- *
257
- * ## Architecture
258
- *
259
- * - Data sources declare their supported chains (async, can change over time)
260
- * - Data sources are responsible for their own update mechanisms (WebSocket, polling, events)
261
- * - The controller does NOT manage polling - it simply receives pushed updates
262
- */
263
- export class AssetsController extends BaseController {
264
- /** Whether the controller is enabled */
265
- #isEnabled;
266
- /** Getter for basic functionality (only balance fetch/subscribe use RPC; token/price API not used). No attribute stored. */
267
- #isBasicFunctionality;
268
- /** Default update interval hint passed to data sources */
269
- #defaultUpdateInterval;
270
- /** Optional trace callback for first init/fetch measurement (duration). */
271
- #trace;
272
- /** Optional reporter for Issue-style errors (e.g. Sentry.captureException). */
273
- #captureException;
274
- /** Whether we have already reported first init fetch for this session (reset on #stop). */
275
- #firstInitFetchReported = false;
276
- /** Whether we have already reported state size for this session (reset on #stop). */
277
- #stateSizeReported = false;
278
- /**
279
- * Emit a state-size trace once on app start (first state update after unlock).
280
- */
281
- #emitStateSizeTrace() {
282
- if (!this.#trace || this.#stateSizeReported) {
283
- return;
284
- }
285
- this.#stateSizeReported = true;
286
- const { assetsBalance: balances, customAssets, assetsInfo, assetsPrice, } = this.state;
287
- // Count balance entries and collect unique asset IDs / chain IDs in one pass.
288
- let balanceEntries = 0;
289
- const uniqueAssets = new Set();
290
- const uniqueNetworks = new Set();
291
- for (const acct of Object.values(balances)) {
292
- const assetIds = Object.keys(acct);
293
- balanceEntries += assetIds.length;
294
- for (const assetId of assetIds) {
295
- uniqueAssets.add(assetId);
296
- // CAIP-19 format: "eip155:1/slip44:60" — chainId is everything before "/"
297
- const slash = assetId.indexOf('/');
298
- if (slash > 0) {
299
- uniqueNetworks.add(assetId.slice(0, slash));
300
- }
301
- }
302
- }
303
- let customAssetEntries = 0;
304
- for (const ids of Object.values(customAssets)) {
305
- if (Array.isArray(ids)) {
306
- customAssetEntries += ids.length;
307
- }
308
- }
309
- emitTrace({
310
- name: TRACE_STATE_SIZE,
311
- trace: this.#trace,
312
- data: {
313
- balance_entries: balanceEntries,
314
- balance_accounts: Object.keys(balances).length,
315
- // `asset_count` matches the Assets Health dashboard widget field name.
316
- asset_count: uniqueAssets.size,
317
- unique_asset_count: uniqueAssets.size,
318
- network_count: uniqueNetworks.size,
319
- metadata_entries: Object.keys(assetsInfo).length,
320
- price_entries: Object.keys(assetsPrice).length,
321
- custom_asset_entries: customAssetEntries,
322
- },
323
- });
324
- }
325
- /** Whether the client (UI) is open. Combined with #keyringUnlocked for #updateActive. */
326
- #uiOpen = false;
327
- /** Whether the keyring is unlocked. Combined with #uiOpen for #updateActive. */
328
- #keyringUnlocked = false;
329
- /**
330
- * Whether `AccountTreeController` has finished `init()`. Unlock / UI-open
331
- * alone must not start fetches — the tree can still be mid-build.
332
- */
333
- #accountTreeInitialized = false;
334
- #controllerMutex = new Mutex();
335
- /** Serializes account-switch fetch + subscribe to prevent overlapping races. */
336
- #accountRefreshMutex = new Mutex();
337
- /**
338
- * Active balance subscriptions keyed by account ID.
339
- * Each account has one logical subscription that may span multiple data sources.
340
- * For example, if WebSocket covers chains A,B and RPC covers chain C,
341
- * the account subscribes to both data sources for its chains.
342
- */
343
- #activeSubscriptions = new Map();
344
- /** Currently enabled chains from NetworkEnablementController */
345
- #enabledChains = new Set();
346
- /**
347
- * Snapshot of account IDs that were active when the last subscription/fetch
348
- * cycle ran. Used by #handleAccountTreeStateChange to skip redundant
349
- * re-subscriptions when the account set hasn't actually changed.
350
- */
351
- #lastKnownAccountIds = new Set();
352
- /**
353
- * Get the currently selected accounts from AccountTreeController.
354
- * This includes all accounts in the same group as the selected account
355
- * (EVM, Bitcoin, Solana, Tron, etc. that belong to the same logical account group).
356
- *
357
- * @returns Array of InternalAccount objects from the selected account group.
358
- */
359
- #getSelectedAccounts() {
360
- const accounts = this.messenger.call('AccountTreeController:getAccountsFromSelectedAccountGroup');
361
- if (accounts.length > 0) {
362
- return accounts;
363
- }
364
- const selectedAccount = this.messenger.call('AccountsController:getSelectedAccount');
365
- if (selectedAccount) {
366
- return [selectedAccount];
367
- }
368
- return [];
369
- }
370
- #accountActivityDataSource;
371
- #accountsApiDataSource;
372
- #snapDataSource;
373
- #rpcDataSource;
374
- #stakedBalanceDataSource;
375
- /**
376
- * All balance data sources in priority order for chain-claiming and cleanup.
377
- * Note: StakedBalanceDataSource is excluded because it provides supplementary
378
- * data and should not participate in chain-claiming.
379
- *
380
- * @returns The four balance data source instances in priority order.
381
- */
382
- get #allBalanceDataSources() {
383
- return [
384
- this.#accountActivityDataSource,
385
- this.#accountsApiDataSource,
386
- this.#snapDataSource,
387
- this.#rpcDataSource,
388
- ];
389
- }
390
- #priceDataSource;
391
- #detectionMiddleware;
392
- #customAssetGraduationMiddleware;
393
- #rpcFallbackMiddleware;
394
- #tokenDataSource;
395
- #unsubscribeBasicFunctionality = null;
396
- #queryApiClient;
397
- #onActiveChainsUpdated;
398
- constructor({ messenger, state = {}, defaultUpdateInterval = DEFAULT_POLLING_INTERVAL_MS, isEnabled = () => true, isBasicFunctionality, subscribeToBasicFunctionalityChange, queryApiClient, rpcDataSourceConfig, trace, captureException, accountsApiDataSourceConfig, priceDataSourceConfig, stakedBalanceDataSourceConfig, isOnboarded, tempMigrateAssetsInfoMetadataAssets3346, }) {
399
- super({
400
- name: CONTROLLER_NAME,
401
- messenger,
402
- metadata: stateMetadata,
403
- state: {
404
- ...getDefaultAssetsControllerState(),
405
- ...state,
406
- },
407
- });
408
- this.#isEnabled = isEnabled;
409
- this.#isBasicFunctionality = isBasicFunctionality ?? (() => true);
410
- this.#defaultUpdateInterval = defaultUpdateInterval;
411
- this.#trace = trace;
412
- this.#captureException = captureException;
413
- this.#queryApiClient = queryApiClient;
414
- const rpcConfig = rpcDataSourceConfig ?? {};
415
- // TEMPORARY: heal assetsInfo metadata wiped by a prior defect
416
- // (see extension migration #215 / ASSETS-3346). Remove in a future release.
417
- if (tempMigrateAssetsInfoMetadataAssets3346) {
418
- this.update(() => tempHealAssetsInfoMetadata({
419
- state: this.state,
420
- getMigrationState: tempMigrateAssetsInfoMetadataAssets3346,
421
- captureException,
422
- }));
423
- }
424
- this.#initializeNativeAssetsMap(queryApiClient);
425
- this.#onActiveChainsUpdated = (dataSourceName, chains, previousChains) => {
426
- try {
427
- this.#handleActiveChainsUpdate(dataSourceName, chains, previousChains);
428
- }
429
- catch (error) {
430
- log('Failed to handle active chains update', {
431
- dataSourceName,
432
- error,
433
- });
434
- }
435
- };
436
- this.#accountActivityDataSource = new AccountActivityDataSource({
437
- messenger: this.messenger,
438
- onActiveChainsUpdated: this.#onActiveChainsUpdated,
439
- getAssetType: (assetId) => this.#getAssetType(assetId),
440
- onAssetsUpdate: (response, request) => this.handleAssetsUpdate(response, this.#accountActivityDataSource.getName(), request),
441
- });
442
- this.#accountsApiDataSource = new AccountsApiDataSource({
443
- messenger: this.messenger,
444
- queryApiClient,
445
- onActiveChainsUpdated: this.#onActiveChainsUpdated,
446
- ...accountsApiDataSourceConfig,
447
- });
448
- this.#snapDataSource = new SnapDataSource({
449
- messenger: this.messenger,
450
- onActiveChainsUpdated: this.#onActiveChainsUpdated,
451
- onAssetsUpdate: (response) => this.handleAssetsUpdate(response, 'SnapDataSource'),
452
- });
453
- this.#rpcDataSource = new RpcDataSource({
454
- messenger: this.messenger,
455
- onActiveChainsUpdated: this.#onActiveChainsUpdated,
456
- getNativeAssetForChain: (chainId) => this.#getNativeAssetMap()[chainId] ??
457
- `${chainId}/erc20:${ZERO_ADDRESS}`,
458
- // Share the API platform's TanStack Query client so the RPC token
459
- // detector caches/dedupes its top-token-list fetches alongside the rest
460
- // of the package's API calls. Caller-provided rpcConfig.queryClient
461
- // wins via the spread below.
462
- queryClient: queryApiClient.queryClient,
463
- ...rpcConfig,
464
- isOnboarded: rpcConfig.isOnboarded ?? isOnboarded,
465
- getAssetType: (assetId) => this.#getAssetType(assetId),
466
- });
467
- this.#stakedBalanceDataSource = new StakedBalanceDataSource({
468
- messenger: this.messenger,
469
- onActiveChainsUpdated: this.#onActiveChainsUpdated,
470
- ...stakedBalanceDataSourceConfig,
471
- });
472
- this.#tokenDataSource = new TokenDataSource(this.messenger, {
473
- queryApiClient,
474
- getNativeAssetIds: () => {
475
- return this.#getNativeAssetIdsForEnabledChains();
476
- },
477
- getAssetType: (assetId) => this.#getAssetType(assetId),
478
- });
479
- this.#priceDataSource = new PriceDataSource({
480
- queryApiClient,
481
- getSelectedCurrency: () => this.state.selectedCurrency,
482
- ...priceDataSourceConfig,
483
- });
484
- this.#detectionMiddleware = new DetectionMiddleware();
485
- this.#customAssetGraduationMiddleware = new CustomAssetGraduationMiddleware({
486
- getSelectedAccountId: () => {
487
- try {
488
- return this.#getSelectedAccounts()[0]?.id;
489
- }
490
- catch {
491
- return undefined;
492
- }
493
- },
494
- removeCustomAsset: (accountId, assetId) => this.removeCustomAsset(accountId, assetId),
495
- });
496
- this.#rpcFallbackMiddleware = new RpcFallbackMiddleware({
497
- rpcDataSource: this.#rpcDataSource,
498
- });
499
- log('Initializing AssetsController', {
500
- defaultUpdateInterval: this.#defaultUpdateInterval,
501
- });
502
- this.#initializeState();
503
- this.#subscribeToEvents();
504
- this.#registerActionHandlers();
505
- // Subscriptions start only when both UI is open and keyring unlocked -> #updateActive().
506
- // Subscribe to basic-functionality changes after construction so a synchronous
507
- // onChange during subscribe cannot run before data sources are initialized.
508
- if (subscribeToBasicFunctionalityChange) {
509
- const unsubscribe = subscribeToBasicFunctionalityChange((isBasic) => this.handleBasicFunctionalityChange(isBasic));
510
- if (typeof unsubscribe === 'function') {
511
- this.#unsubscribeBasicFunctionality = unsubscribe;
512
- }
513
- }
514
- }
515
- // ============================================================================
516
- // INITIALIZATION
517
- // ============================================================================
518
- #initializeNativeAssetsMap(queryApiClient) {
519
- queryApiClient.queryClient
520
- .fetchQuery({
521
- queryKey: NATIVE_ASSETS_QUERY_KEY,
522
- queryFn: buildNativeAssetsFromApi,
523
- staleTime: Infinity,
524
- gcTime: Infinity,
525
- })
526
- .catch((error) => {
527
- // Failure to populate native asset cache is non-fatal;
528
- // #isNativeAsset falls back to the seed data from buildNativeAssetsFromConstant.
529
- log('Failed to populate native asset cache, falling back to seed data', error);
530
- });
531
- // Seed the cache synchronously so that synchronous consumers (e.g.
532
- // #isNativeAsset, #resolveNativeAssetIds) have data available immediately.
533
- // When fetchQuery resolves (currently instant, future: API), it overwrites
534
- // this with potentially fresher data.
535
- const initialNativeAssetMap = buildNativeAssetsFromConstant();
536
- queryApiClient.setCachedData(NATIVE_ASSETS_QUERY_KEY, initialNativeAssetMap);
537
- }
538
- #initializeState() {
539
- const { enabledNetworkMap } = this.messenger.call('NetworkEnablementController:getState');
540
- this.#enabledChains = this.#extractEnabledChains(enabledNetworkMap);
541
- log('Initialized state', {
542
- enabledNetworkMap,
543
- enabledChains: this.#enabledChains,
544
- });
545
- }
546
- /**
547
- * Extract enabled chains from enabledNetworkMap.
548
- * Returns CAIP-2 chain IDs for all enabled networks across all namespaces.
549
- *
550
- * Note: For EIP155 (EVM) chains, the reference is normalized to decimal format
551
- * to ensure consistency with CAIP-2 standard and API responses.
552
- *
553
- * @param enabledNetworkMap - The enabled network map from NetworkEnablementController.
554
- * @returns Set of CAIP-2 chain IDs for all enabled networks.
555
- */
556
- #extractEnabledChains(enabledNetworkMap) {
557
- const chains = new Set();
558
- for (const [namespace, networks] of Object.entries(enabledNetworkMap)) {
559
- for (const [reference, isEnabled] of Object.entries(networks)) {
560
- if (isEnabled) {
561
- // Check if reference is already a full CAIP-2 chain ID (contains colon)
562
- if (reference.includes(':')) {
563
- // Already a full chain ID, use as-is
564
- chains.add(reference);
565
- }
566
- else {
567
- // Normalize EIP155 chain references from hex to decimal (CAIP-2 standard)
568
- const normalizedReference = this.#normalizeChainReference(namespace, reference);
569
- chains.add(`${namespace}:${normalizedReference}`);
570
- }
571
- }
572
- }
573
- }
574
- return chains;
575
- }
576
- /**
577
- * Normalize chain reference to CAIP-2 standard format.
578
- * For EIP155, converts hex chain IDs to decimal.
579
- *
580
- * @param namespace - The chain namespace (e.g., "eip155").
581
- * @param reference - The chain reference (e.g., "0x1" or "1").
582
- * @returns The normalized chain reference in decimal format.
583
- */
584
- #normalizeChainReference(namespace, reference) {
585
- if (namespace === 'eip155' && isStrictHexString(reference)) {
586
- // Convert hex to decimal for EIP155 chains
587
- return parseInt(reference, 16).toString();
588
- }
589
- return reference;
590
- }
591
- #subscribeToEvents() {
592
- // Subscribe to account group changes (when user switches between account groups like Account 1 -> Account 2)
593
- this.messenger.subscribe('AccountTreeController:selectedAccountGroupChange', (groupId, previousGroupId) => {
594
- this.#handleAccountGroupChanged(groupId, previousGroupId).catch(console.error);
595
- });
596
- // Catch post-init tree mutations that change the selected account set
597
- // (e.g. a snap account added to the current group) without changing
598
- // the selected group id. Intermediate `:stateChange` events during
599
- // `AccountTreeController.init()` are ignored until `:initialized`.
600
- this.messenger.subscribe('AccountTreeController:stateChange', () => {
601
- this.#handleAccountTreeStateChange();
602
- });
603
- // Subscribe to network enablement changes (only enabledNetworkMap)
604
- this.messenger.subscribe('NetworkEnablementController:stateChange', ({ enabledNetworkMap }) => {
605
- this.#handleEnabledNetworksChanged(enabledNetworkMap).catch(console.error);
606
- });
607
- // When a network is added or removed from NetworkController, refresh
608
- // assets across every data source so balances, prices, and metadata
609
- // stay consistent. On add we also seed default tracked assets (e.g.
610
- // mUSD on Monad) when the chain is in our defaults registry, so the
611
- // entries appear immediately without waiting for it to also be
612
- // enabled in NetworkEnablementController.
613
- this.messenger.subscribe('NetworkController:networkAdded', (networkConfiguration) => {
614
- this.#handleNetworkAdded(networkConfiguration.chainId);
615
- this.#refreshAssetsAfterNetworkAdded(networkConfiguration.chainId).catch((error) => {
616
- log('Failed to refresh assets after network added', { error });
617
- });
618
- });
619
- this.messenger.subscribe('NetworkController:networkRemoved', () => {
620
- this.#refreshAssetsAfterNetworkChange();
621
- });
622
- // Selected EVM network switch (network picker). Enablement changes are
623
- // handled separately via NetworkEnablementController:stateChange.
624
- this.messenger.subscribe('NetworkController:networkDidChange', (networkState) => {
625
- this.#handleNetworkDidChange(networkState).catch(console.error);
626
- });
627
- // Client + Keyring lifecycle: only run when UI is open AND keyring is unlocked
628
- this.messenger.subscribe('ClientController:stateChange', (isUiOpen) => {
629
- this.#uiOpen = isUiOpen;
630
- this.#updateActive();
631
- }, clientControllerSelectors.selectIsUiOpen);
632
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
633
- this.messenger.subscribe('KeyringController:unlock', async () => {
634
- this.#keyringUnlocked = true;
635
- await this.#runSpamCleanup().catch(() => {
636
- /* Do nothing */
637
- });
638
- this.#updateActive();
639
- });
640
- this.messenger.subscribe('KeyringController:lock', () => {
641
- this.#keyringUnlocked = false;
642
- this.#updateActive();
643
- });
644
- // Subscribe to unapproved transactions - TXs that need confirmation
645
- // Ensures that balances for the account making transaction are updated (e.g. for gas estimations)
646
- this.messenger.subscribe('TransactionController:unapprovedTransactionAdded', (transactionMeta) => {
647
- this.#onUnapprovedTransactionAdded(transactionMeta);
648
- });
649
- // Post-tx refresh via the full fetch pipeline (Accounts API + RPC fallback).
650
- // RpcDataSource also listens for transactionConfirmed, but only refreshes
651
- // chains it owns via an active subscription.
652
- this.messenger.subscribe('TransactionController:transactionConfirmed', (transactionMeta) => {
653
- this.#onTransactionConfirmed(transactionMeta);
654
- });
655
- // Start tracking only after the account tree is fully built. Unlock can
656
- // happen before `AccountTreeController.init()`, and `:stateChange` fires
657
- // for intermediate mutations during that build.
658
- this.messenger.subscribe('AccountTreeController:initialized', () => {
659
- this.#accountTreeInitialized = true;
660
- this.#updateActive();
661
- });
662
- this.messenger.subscribe('AccountTreeController:uninitialized', () => {
663
- this.#accountTreeInitialized = false;
664
- this.#updateActive();
665
- });
666
- }
667
- #onUnapprovedTransactionAdded(transactionMeta) {
668
- const hexChainId = transactionMeta.chainId;
669
- if (!hexChainId) {
670
- return;
671
- }
672
- const caipChainId = `eip155:${parseInt(hexChainId, 16)}`;
673
- const fromAddress = transactionMeta.txParams.from?.toLowerCase();
674
- if (!fromAddress) {
675
- return;
676
- }
677
- const matchedAccount = this.#getSelectedAccounts().find((account) => account.address.toLowerCase() === fromAddress);
678
- if (!matchedAccount) {
679
- return;
680
- }
681
- this.getAssets([matchedAccount], {
682
- chainIds: [caipChainId],
683
- forceUpdate: true,
684
- }).catch((error) => {
685
- log('Failed to refresh assets after unapproved transaction added', {
686
- error,
687
- });
688
- });
689
- }
690
- #onTransactionConfirmed(transactionMeta) {
691
- const hexChainId = transactionMeta.chainId;
692
- if (!hexChainId) {
693
- return;
694
- }
695
- const caipChainId = `eip155:${parseInt(hexChainId, 16)}`;
696
- const fromAddress = transactionMeta.txParams.from?.toLowerCase();
697
- if (!fromAddress) {
698
- return;
699
- }
700
- const matchedAccount = this.#getSelectedAccounts().find((account) => account.address.toLowerCase() === fromAddress);
701
- if (!matchedAccount) {
702
- return;
703
- }
704
- this.getAssets([matchedAccount], {
705
- chainIds: [caipChainId],
706
- forceUpdate: true,
707
- }).catch((error) => {
708
- log('Failed to refresh assets after transaction confirmed', { error });
709
- });
710
- }
711
- async #runSpamCleanup() {
712
- try {
713
- const shouldRun = this.#keyringUnlocked &&
714
- this.#isBasicFunctionality() &&
715
- isUnlockCleanupEnabled(this.messenger.call('RemoteFeatureFlagController:getState')
716
- ?.remoteFeatureFlags);
717
- if (!shouldRun) {
718
- return;
719
- }
720
- }
721
- catch (error) {
722
- log('Failed to start spam cleanup', { error });
723
- return;
724
- }
725
- try {
726
- const originalState = this.state;
727
- const result = await cleanSpamAssets({
728
- state: originalState,
729
- apiClient: this.#queryApiClient,
730
- captureException: this.#captureException,
731
- });
732
- if (!result) {
733
- return;
734
- }
735
- this.update((state) => {
736
- result.applyPatch(state, {
737
- spamAssetIds: result.spamAssetIds,
738
- });
739
- });
740
- }
741
- catch (error) {
742
- log('Failed to run spam cleanup', { error });
743
- }
744
- }
745
- /**
746
- * Start or stop asset tracking based on client (UI) open state, keyring
747
- * unlock state, and account-tree readiness. Only runs when the UI is open,
748
- * the keyring is unlocked, and the account tree has finished `init()`.
749
- */
750
- #updateActive() {
751
- const shouldRun = this.#uiOpen && this.#keyringUnlocked && this.#accountTreeInitialized;
752
- if (shouldRun) {
753
- this.#start();
754
- }
755
- else {
756
- this.#stop();
757
- }
758
- }
759
- /**
760
- * Handle AccountTreeController state changes after the tree is initialized.
761
- * Re-subscribe only when the set of selected accounts has actually changed
762
- * (e.g. a snap account was added after initial startup). Intermediate
763
- * `:stateChange` events during `init()` are ignored — startup is driven by
764
- * `:initialized` instead, so we do not fetch on every tree mutation.
765
- */
766
- #handleAccountTreeStateChange() {
767
- const shouldRun = this.#uiOpen &&
768
- this.#keyringUnlocked &&
769
- this.#accountTreeInitialized &&
770
- this.#activeSubscriptions.size > 0;
771
- if (!shouldRun) {
772
- return;
773
- }
774
- const accounts = this.#getSelectedAccounts();
775
- const currentIds = new Set(accounts.map((a) => a.id));
776
- const accountsChanged = currentIds.size !== this.#lastKnownAccountIds.size ||
777
- [...currentIds].some((id) => !this.#lastKnownAccountIds.has(id));
778
- if (!accountsChanged) {
779
- return;
780
- }
781
- const hasOverlap = [...currentIds].some((id) => this.#lastKnownAccountIds.has(id));
782
- if (!hasOverlap && this.#lastKnownAccountIds.size > 0) {
783
- return;
784
- }
785
- log('Account tree changed with new accounts, re-subscribing', {
786
- previousCount: this.#lastKnownAccountIds.size,
787
- currentCount: currentIds.size,
788
- });
789
- const newAccounts = accounts.filter((account) => !this.#lastKnownAccountIds.has(account.id));
790
- this.#lastKnownAccountIds = currentIds;
791
- this.#ensureNativeBalancesDefaultZero();
792
- this.#ensureDefaultTrackedAssetsSeeded();
793
- this.#runAccountTreeRefresh(accounts, newAccounts).catch((error) => {
794
- log('Failed to refresh assets after tree change', error);
795
- });
796
- }
797
- async #runAccountTreeRefresh(accounts, newAccounts = []) {
798
- const releaseLock = await this.#accountRefreshMutex.acquire();
799
- try {
800
- await this.getAssets(accounts, {
801
- chainIds: [...this.#enabledChains],
802
- forceUpdate: true,
803
- });
804
- this.#subscribeAssets({ skipInitialFetch: true });
805
- if (newAccounts.length > 0) {
806
- await this.getAssets(newAccounts, {
807
- chainIds: [...this.#enabledChains],
808
- forceUpdate: true,
809
- });
810
- }
811
- this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]);
812
- }
813
- catch (error) {
814
- log('Failed to fetch assets after tree change', error);
815
- this.#subscribeAssets({ skipInitialFetch: true });
816
- this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]);
817
- }
818
- finally {
819
- releaseLock();
820
- }
821
- }
822
- /**
823
- * Force-fetch balances then subscribe. Used on unlock / first startup.
824
- *
825
- * @param accounts - Selected accounts to refresh.
826
- */
827
- async #runStartupRefresh(accounts) {
828
- const releaseLock = await this.#accountRefreshMutex.acquire();
829
- try {
830
- await this.getAssets(accounts, {
831
- chainIds: [...this.#enabledChains],
832
- forceUpdate: true,
833
- });
834
- // Seed before subscribe so the price poll / update fetch sees natives
835
- // and default tracked assets that were never returned by balance APIs.
836
- this.#ensureNativeBalancesDefaultZero();
837
- this.#ensureDefaultTrackedAssetsSeeded();
838
- // Balances were just force-fetched — skip AccountsApi's subscribe-time poll.
839
- this.#subscribeAssets({ skipInitialFetch: true });
840
- this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]);
841
- }
842
- catch (error) {
843
- log('Failed to fetch assets on startup', error);
844
- this.#ensureNativeBalancesDefaultZero();
845
- this.#ensureDefaultTrackedAssetsSeeded();
846
- this.#subscribeAssets({ skipInitialFetch: true });
847
- this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]);
848
- }
849
- finally {
850
- releaseLock();
851
- }
852
- }
853
- #registerActionHandlers() {
854
- this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
855
- }
856
- // ============================================================================
857
- // DATA SOURCE CHAIN MANAGEMENT
858
- // ============================================================================
859
- /**
860
- * Handle when a data source's supported chains change.
861
- * Used to refresh balance subscriptions and run a one-time fetch when a new chain is supported.
862
- *
863
- * - On any add/remove: re-subscribes to data sources so chain assignment stays correct.
864
- * - When chains are added: fetches balances for the new chains (for selected accounts on enabled networks).
865
- *
866
- * Controller does not store chains; sources report via this callback. previousChains is required for diff.
867
- *
868
- * @param dataSourceId - The identifier of the data source reporting the change.
869
- * @param activeChains - Currently active (supported and available) chain IDs for this source.
870
- * @param previousChains - Previous chains; used to compute added/removed.
871
- */
872
- #handleActiveChainsUpdate(dataSourceId, activeChains, previousChains) {
873
- if (!this.#uiOpen || !this.#keyringUnlocked || !this.#isEnabled()) {
874
- return;
875
- }
876
- log('Data source active chains changed', {
877
- dataSourceId,
878
- chainCount: activeChains.length,
879
- chains: activeChains,
880
- });
881
- const previous = previousChains;
882
- const previousSet = new Set(previous);
883
- const addedChains = activeChains.filter((ch) => !previousSet.has(ch));
884
- const removedChains = previous.filter((ch) => !activeChains.includes(ch));
885
- // Refresh subscriptions to use updated data source availability.
886
- if (addedChains.length > 0 || removedChains.length > 0) {
887
- this.#subscribeAssets();
888
- }
889
- }
890
- /**
891
- * Returns the callback passed to data sources for reporting active chain updates.
892
- * Used by tests to simulate a data source reporting chain changes.
893
- *
894
- * @returns The onActiveChainsUpdated callback.
895
- */
896
- getOnActiveChainsUpdated() {
897
- return this.#onActiveChainsUpdated;
898
- }
899
- // ============================================================================
900
- // MIDDLEWARE EXECUTION
901
- // ============================================================================
902
- /**
903
- * Execute middlewares with request/response context.
904
- * Returns response and exclusive duration per source (sum ≈ wall time).
905
- *
906
- * @param params - Middleware execution options.
907
- * @param params.sources - Data sources or middlewares with getName() and assetsMiddleware.
908
- * @param params.request - The data request.
909
- * @param params.initialResponse - Optional initial response (for enriching existing data).
910
- * @param params.parentContext - Optional parent Sentry span; per-source timings nest under it.
911
- * @param params.trace - Optional trace callback; omit after unlock/first-init to skip spans.
912
- * @returns Response and durationByDataSource (exclusive ms per source name).
913
- */
914
- async #executeMiddlewares(params) {
915
- const { sources, request, initialResponse = {}, parentContext, trace, } = params;
916
- const names = sources.map((source) => source.getName());
917
- const middlewares = sources.map((source) => source.assetsMiddleware);
918
- const inclusive = [];
919
- const wrapped = middlewares.map((middleware, i) => (async (ctx, next) => {
920
- const start = performance.now();
921
- try {
922
- return await middleware(ctx, next);
923
- }
924
- finally {
925
- inclusive[i] = performance.now() - start;
926
- }
927
- }));
928
- const middlewareErrors = [];
929
- const chain = wrapped.reduceRight((next, middleware, index) => async (ctx) => {
930
- try {
931
- return await middleware(ctx, next);
932
- }
933
- catch (error) {
934
- const sourceName = names[index] ?? `middleware_${index}`;
935
- middlewareErrors.push(sourceName);
936
- console.error('[AssetsController] Middleware failed:', error);
937
- return next(ctx);
938
- }
939
- }, async (ctx) => ctx);
940
- const result = await chain({
941
- request,
942
- response: initialResponse,
943
- getAssetsState: () => this.state,
944
- });
945
- const durationByDataSource = {};
946
- for (let i = 0; i < inclusive.length; i++) {
947
- const nextInc = i + 1 < inclusive.length ? (inclusive[i + 1] ?? 0) : 0;
948
- const exclusive = Math.max(0, (inclusive[i] ?? 0) - nextInc);
949
- const name = names[i];
950
- if (name !== undefined) {
951
- durationByDataSource[name] = exclusive;
952
- }
953
- }
954
- if (result.durationByDataSource) {
955
- for (const [key, ms] of Object.entries(result.durationByDataSource)) {
956
- durationByDataSource[key] = ms;
957
- }
958
- }
959
- // Emit per-source timing as subspans under the parent fetch/update span
960
- // (no-op when `trace` is omitted — unlock/first-init only).
961
- for (const [sourceName, durationMs] of Object.entries(durationByDataSource)) {
962
- emitTrace({
963
- name: TRACE_DATA_SOURCE_TIMING,
964
- trace,
965
- data: {
966
- source: sourceName,
967
- duration_ms: durationMs,
968
- chain_count: request.chainIds.length,
969
- account_count: request.accountsWithSupportedChains.length,
970
- },
971
- tags: {
972
- controller: 'AssetsController',
973
- // String tag so Spans widgets can group by `source`.
974
- source: sourceName,
975
- },
976
- parentContext,
977
- });
978
- }
979
- // Failed middlewares: Issues (optional) + perf/Dashboard spans
980
- if (middlewareErrors.length > 0) {
981
- const failedSources = middlewareErrors.join(',');
982
- const assetsError = new AssetsDataSourceError({
983
- failedSources,
984
- errorCount: middlewareErrors.length,
985
- chainCount: request.chainIds.length,
986
- });
987
- try {
988
- this.#captureException?.(assetsError);
989
- }
990
- catch {
991
- // Never let telemetry throw.
992
- }
993
- emitTrace({
994
- name: TRACE_DATA_SOURCE_ERROR,
995
- trace,
996
- data: {
997
- failed_sources: failedSources,
998
- error_count: middlewareErrors.length,
999
- chain_count: request.chainIds.length,
1000
- },
1001
- tags: {
1002
- controller: 'AssetsController',
1003
- severity: 'error',
1004
- error_type: assetsError.name,
1005
- },
1006
- parentContext,
1007
- });
1008
- }
1009
- return { response: result.response, durationByDataSource };
1010
- }
1011
- // ============================================================================
1012
- // PUBLIC API: QUERY METHODS
1013
- // ============================================================================
1014
- async getAssets(accounts, options) {
1015
- const chainIds = options?.chainIds ?? [...this.#enabledChains];
1016
- const assetTypes = options?.assetTypes ?? ['fungible'];
1017
- const dataTypes = options?.dataTypes ?? ['balance', 'metadata', 'price'];
1018
- if (accounts.length === 0 || chainIds.length === 0) {
1019
- return this.#getAssetsFromState(accounts, chainIds, assetTypes);
1020
- }
1021
- // Collect custom assets for all requested accounts
1022
- const customAssets = [];
1023
- for (const account of accounts) {
1024
- const accountCustomAssets = this.getCustomAssets(account.id);
1025
- customAssets.push(...accountCustomAssets);
1026
- }
1027
- if (options?.forceUpdate) {
1028
- // Pipeline spans only on unlock/first-init fetch; later forceUpdates pass
1029
- // `undefined` so `emitTrace` / `withTrace` no-op without call-site if/else.
1030
- const pipelineTrace = this.#firstInitFetchReported
1031
- ? undefined
1032
- : this.#trace;
1033
- const request = this.#buildDataRequest(accounts, chainIds, {
1034
- assetTypes,
1035
- dataTypes,
1036
- customAssets: customAssets.length > 0 ? customAssets : undefined,
1037
- forceUpdate: true,
1038
- assetsForPriceUpdate: options?.assetsForPriceUpdate,
1039
- });
1040
- // Fast pipeline: accountsApi + stakedBalance → detection → token + price.
1041
- // Snap and RPC are excluded here due to their latency (snap triggers account
1042
- // creation, RPC is slow on many chains). Results are committed to state
1043
- // immediately so the UI can display balances without waiting for them.
1044
- //
1045
- // Fast/slow pipelines use merge so partial API snapshots cannot wipe
1046
- // tokens missing from the response (e.g. USDC when only native balance
1047
- // is returned). Balances present in the response are still refreshed.
1048
- const fastSources = this.#isBasicFunctionality()
1049
- ? [
1050
- createParallelBalanceMiddleware([
1051
- this.#accountsApiDataSource,
1052
- this.#stakedBalanceDataSource,
1053
- ]),
1054
- // Graduation must run BEFORE the RPC fallback so it only sees
1055
- // AccountsApi/Websocket balances. RPC intentionally carries
1056
- // custom assets and must never trigger graduation.
1057
- this.#customAssetGraduationMiddleware,
1058
- this.#rpcFallbackMiddleware,
1059
- this.#detectionMiddleware,
1060
- createParallelMiddleware([
1061
- this.#tokenDataSource,
1062
- this.#priceDataSource,
1063
- ]),
1064
- ]
1065
- : [this.#stakedBalanceDataSource, this.#detectionMiddleware];
1066
- const { response } = await withTrace({
1067
- name: TRACE_FETCH_PIPELINE,
1068
- trace: pipelineTrace,
1069
- data: {
1070
- chain_count: chainIds.length,
1071
- account_count: accounts.length,
1072
- basic_functionality: this.#isBasicFunctionality(),
1073
- },
1074
- fn: async (parentContext) => {
1075
- const startTime = performance.now();
1076
- const result = await this.#executeMiddlewares({
1077
- sources: fastSources,
1078
- request,
1079
- parentContext,
1080
- trace: pipelineTrace,
1081
- });
1082
- await this.#updateState({
1083
- ...result.response,
1084
- updateMode: 'merge',
1085
- replaceCoveredChainBalances: true,
1086
- });
1087
- const durationMs = performance.now() - startTime;
1088
- // Summary fields for Assets Health (nested under the parent span).
1089
- emitTrace({
1090
- name: TRACE_FULL_FETCH,
1091
- trace: pipelineTrace,
1092
- data: {
1093
- duration_ms: durationMs,
1094
- chain_count: chainIds.length,
1095
- account_count: accounts.length,
1096
- basic_functionality: this.#isBasicFunctionality(),
1097
- asset_count: result.response.assetsBalance
1098
- ? Object.values(result.response.assetsBalance).reduce((sum, acct) => sum + Object.keys(acct).length, 0)
1099
- : 0,
1100
- price_count: result.response.assetsPrice
1101
- ? Object.keys(result.response.assetsPrice).length
1102
- : 0,
1103
- ...result.durationByDataSource,
1104
- },
1105
- parentContext,
1106
- });
1107
- emitTrace({
1108
- name: TRACE_FIRST_INIT_FETCH,
1109
- trace: pipelineTrace,
1110
- data: {
1111
- duration_ms: durationMs,
1112
- chain_ids: JSON.stringify(chainIds),
1113
- ...result.durationByDataSource,
1114
- },
1115
- parentContext,
1116
- });
1117
- return result;
1118
- },
1119
- });
1120
- // Mark after the unlock/first forceUpdate so later polls skip pipeline spans.
1121
- this.#firstInitFetchReported = true;
1122
- // Background (slow) lane — flattened sibling of the fast lane (not nested
1123
- // inside its withTrace callback). Still fire-and-forget so UI is not blocked.
1124
- const slowPipelineChainIds = this.#getSlowPipelineChainIds(chainIds, response);
1125
- if (slowPipelineChainIds.length > 0) {
1126
- const slowSources = this.#isBasicFunctionality()
1127
- ? [this.#snapDataSource, this.#rpcDataSource]
1128
- : [this.#rpcDataSource];
1129
- const slowRequest = { ...request, chainIds: slowPipelineChainIds };
1130
- withTrace({
1131
- name: TRACE_BACKGROUND_FETCH,
1132
- trace: pipelineTrace,
1133
- data: {
1134
- chain_count: slowPipelineChainIds.length,
1135
- account_count: accounts.length,
1136
- },
1137
- fn: async (slowParentContext) => {
1138
- const { response: slowResponse } = await this.#executeMiddlewares({
1139
- sources: [
1140
- createParallelBalanceMiddleware(slowSources),
1141
- this.#detectionMiddleware,
1142
- ...(this.#isBasicFunctionality()
1143
- ? [
1144
- createParallelMiddleware([
1145
- this.#tokenDataSource,
1146
- this.#priceDataSource,
1147
- ]),
1148
- ]
1149
- : []),
1150
- ],
1151
- request: slowRequest,
1152
- parentContext: slowParentContext,
1153
- trace: pipelineTrace,
1154
- });
1155
- await this.#updateState({
1156
- ...slowResponse,
1157
- updateMode: 'merge',
1158
- });
1159
- },
1160
- }).catch((error) => log('Background pipeline failed', { error }));
1161
- }
1162
- }
1163
- const result = this.#getAssetsFromState(accounts, chainIds, assetTypes);
1164
- return result;
1165
- }
1166
- async getAssetsBalance(accounts, options) {
1167
- // Reuse getAssets with dataTypes: ['balance'] only
1168
- const assets = await this.getAssets(accounts, {
1169
- chainIds: options?.chainIds,
1170
- assetTypes: options?.assetTypes,
1171
- forceUpdate: options?.forceUpdate,
1172
- dataTypes: ['balance', 'metadata'],
1173
- });
1174
- // Extract just the balance from each asset
1175
- const result = {};
1176
- for (const [accountId, accountAssets] of Object.entries(assets)) {
1177
- result[accountId] = {};
1178
- for (const [assetId, asset] of Object.entries(accountAssets)) {
1179
- if (asset.balance) {
1180
- result[accountId][assetId] = asset.balance;
1181
- }
1182
- }
1183
- }
1184
- return result;
1185
- }
1186
- getAssetMetadata(assetId) {
1187
- return this.state.assetsInfo[assetId];
1188
- }
1189
- /**
1190
- * Get a single combined asset (balance + metadata + price + computed
1191
- * `fiatValue`) for an account directly from controller state.
1192
- *
1193
- * Reuses the same state-composition and filtering logic as `getAssets`
1194
- * (balance and metadata are required, a missing price falls back to
1195
- * `{ price: 0, lastUpdated: 0 }` with `fiatValue: 0`, and hidden or
1196
- * otherwise filtered assets are excluded) so the returned shape never
1197
- * drifts from `getAssets`. Reads from current state only and does not
1198
- * trigger a data-source refresh.
1199
- *
1200
- * @param accountId - The account ID (`InternalAccount.id`, not an address).
1201
- * @param assetId - The CAIP-19 asset ID including chain scope
1202
- * (e.g. `eip155:1/erc20:0x...`).
1203
- * @returns The combined `Asset`, or `undefined` when no complete
1204
- * renderable asset (balance + metadata) exists for the account/asset pair.
1205
- * @throws If `accountId` is empty or `assetId` is not a valid CAIP-19 asset ID.
1206
- */
1207
- getAccountAssetByID(accountId, assetId) {
1208
- this.#assertNonEmptyAccountId(accountId, 'getAccountAssetByID');
1209
- const normalizedAssetId = this.#normalizeAssetIdOrThrow(assetId, 'getAccountAssetByID');
1210
- return this.#getAssetFromState(accountId, normalizedAssetId, {
1211
- assetTypeSet: new Set(['fungible']),
1212
- });
1213
- }
1214
- /**
1215
- * Get multiple combined assets (balance + metadata + price + computed
1216
- * `fiatValue`) for an account directly from controller state.
1217
- *
1218
- * Applies the same state-composition and filtering rules as
1219
- * `getAccountAssetByID` to each requested asset ID. Asset IDs that do not
1220
- * resolve to a complete renderable asset (missing balance/metadata, hidden,
1221
- * or otherwise filtered out) are omitted from the result. Reads from
1222
- * current state only and does not trigger a data-source refresh.
1223
- *
1224
- * @param accountId - The account ID (`InternalAccount.id`, not an address).
1225
- * @param assetIds - The CAIP-19 asset IDs including chain scope
1226
- * (e.g. `eip155:1/erc20:0x...`).
1227
- * @returns A record of combined `Asset`s keyed by normalized CAIP-19 asset
1228
- * ID, containing only the requested assets that resolved.
1229
- * @throws If `accountId` is empty or any `assetIds` entry is not a valid
1230
- * CAIP-19 asset ID.
1231
- */
1232
- getAccountAssetsByIDs(accountId, assetIds) {
1233
- this.#assertNonEmptyAccountId(accountId, 'getAccountAssetsByIDs');
1234
- const result = Object.create(null);
1235
- for (const assetId of assetIds) {
1236
- const normalizedAssetId = this.#normalizeAssetIdOrThrow(assetId, 'getAccountAssetsByIDs');
1237
- const asset = this.#getAssetFromState(accountId, normalizedAssetId, {
1238
- assetTypeSet: new Set(['fungible']),
1239
- });
1240
- if (asset) {
1241
- result[normalizedAssetId] = asset;
1242
- }
1243
- }
1244
- return result;
1245
- }
1246
- /**
1247
- * Get all combined assets (balance + metadata + price + computed
1248
- * `fiatValue`) an account holds on a given chain scope, directly from
1249
- * controller state.
1250
- *
1251
- * Applies the same state-composition and filtering rules as
1252
- * `getAccountAssetByID` (hidden or otherwise filtered assets are excluded).
1253
- * Reads from current state only and does not trigger a data-source refresh.
1254
- *
1255
- * @param accountId - The account ID (`InternalAccount.id`, not an address).
1256
- * @param scope - The CAIP-2 chain ID to filter by (e.g. `eip155:1`).
1257
- * @returns A record of combined `Asset`s keyed by CAIP-19 asset ID,
1258
- * containing only the account's assets on the given scope.
1259
- * @throws If `accountId` is empty or `scope` is not a valid CAIP-2 chain ID.
1260
- */
1261
- getAccountAssetsByScope(accountId, scope) {
1262
- this.#assertNonEmptyAccountId(accountId, 'getAccountAssetsByScope');
1263
- if (!isCaipChainId(scope)) {
1264
- throw new Error(`AssetsController.getAccountAssetsByScope: invalid CAIP-2 scope "${String(scope)}"`);
1265
- }
1266
- const result = Object.create(null);
1267
- const chainIdSet = new Set([scope]);
1268
- const assetTypeSet = new Set(['fungible']);
1269
- const accountBalances = this.state.assetsBalance[accountId] ?? {};
1270
- for (const assetId of Object.keys(accountBalances)) {
1271
- const typedAssetId = assetId;
1272
- const asset = this.#getAssetFromState(accountId, typedAssetId, {
1273
- chainIdSet,
1274
- assetTypeSet,
1275
- });
1276
- if (asset) {
1277
- result[typedAssetId] = asset;
1278
- }
1279
- }
1280
- return result;
1281
- }
1282
- /**
1283
- * Throws when the provided account ID is not a non-empty string.
1284
- *
1285
- * @param accountId - The account ID to validate.
1286
- * @param methodName - The public method name used in the error message.
1287
- */
1288
- #assertNonEmptyAccountId(accountId, methodName) {
1289
- if (typeof accountId !== 'string' || accountId.length === 0) {
1290
- throw new Error(`AssetsController.${methodName}: accountId must be a non-empty string`);
1291
- }
1292
- }
1293
- /**
1294
- * Normalizes a CAIP-19 asset ID, converting normalization failures into a
1295
- * descriptive error.
1296
- *
1297
- * @param assetId - The CAIP-19 asset ID to normalize.
1298
- * @param methodName - The public method name used in the error message.
1299
- * @returns The normalized CAIP-19 asset ID.
1300
- */
1301
- #normalizeAssetIdOrThrow(assetId, methodName) {
1302
- try {
1303
- return normalizeAssetId(assetId);
1304
- }
1305
- catch {
1306
- throw new Error(`AssetsController.${methodName}: invalid CAIP-19 assetId "${assetId}"`);
1307
- }
1308
- }
1309
- async getAssetsPrice(accounts, options) {
1310
- const assets = await this.getAssets(accounts, {
1311
- chainIds: options?.chainIds,
1312
- assetTypes: options?.assetTypes,
1313
- forceUpdate: options?.forceUpdate,
1314
- dataTypes: ['price'],
1315
- });
1316
- // Extract just the price from each asset (flattened across accounts)
1317
- const result = {};
1318
- for (const accountAssets of Object.values(assets)) {
1319
- for (const [assetId, asset] of Object.entries(accountAssets)) {
1320
- if (asset.price) {
1321
- result[assetId] = asset.price;
1322
- }
1323
- }
1324
- }
1325
- return result;
1326
- }
1327
- /**
1328
- * Returns exchange rates in the format expected by the bridge controller
1329
- * (conversionRates, currencyRates, marketData, currentCurrency) so that
1330
- * when useAssetsControllerForRates is true the bridge can use a single
1331
- * action instead of MultichainAssetsRatesController, TokenRatesController,
1332
- * and CurrencyRateController.
1333
- *
1334
- * @returns Bridge-compatible exchange rate state derived from assetsPrice and selectedCurrency.
1335
- */
1336
- getExchangeRatesForBridge() {
1337
- const { networkConfigurationsByChainId } = this.messenger.call('NetworkController:getState');
1338
- return formatExchangeRatesForBridge({
1339
- assetsInfo: this.state.assetsInfo,
1340
- assetsPrice: this.state.assetsPrice,
1341
- selectedCurrency: this.state.selectedCurrency,
1342
- nativeAssetIdentifiers: this.#getNativeAssetMap(),
1343
- networkConfigurationsByChainId,
1344
- });
1345
- }
1346
- /**
1347
- * Returns state in the legacy format expected by transaction-pay-controller
1348
- * (TokenBalancesController, AccountTrackerController, TokensController,
1349
- * TokenRatesController, CurrencyRateController shapes) so that when
1350
- * useAssetsController is true the transaction-pay-controller can use a
1351
- * single action instead of five separate getState calls.
1352
- *
1353
- * @returns Legacy-compatible state for transaction-pay-controller.
1354
- */
1355
- getStateForTransactionPay() {
1356
- const accounts = this.#getSelectedAccounts();
1357
- const { networkConfigurationsByChainId } = this.messenger.call('NetworkController:getState');
1358
- return formatStateForTransactionPay({
1359
- assetsBalance: this.state.assetsBalance,
1360
- assetsInfo: this.state.assetsInfo,
1361
- assetsPrice: this.state.assetsPrice,
1362
- selectedCurrency: this.state.selectedCurrency,
1363
- accounts: accounts.map((a) => ({ id: a.id, address: a.address })),
1364
- nativeAssetIdentifiers: this.#getNativeAssetMap(),
1365
- networkConfigurationsByChainId,
1366
- });
1367
- }
1368
- // ============================================================================
1369
- // CUSTOM ASSETS MANAGEMENT
1370
- // ============================================================================
1371
- /**
1372
- * Add a custom asset for an account.
1373
- * Custom assets are included in subscription and fetch operations.
1374
- * Adding a custom asset also unhides it if it was previously hidden.
1375
- *
1376
- * When `pendingMetadata` is provided (e.g. from the extension's pending-tokens
1377
- * flow), the token metadata is persisted immediately into `assetsInfo` so the
1378
- * UI can render it without waiting for the next pipeline fetch.
1379
- *
1380
- * @param accountId - The account ID to add the custom asset for.
1381
- * @param assetId - The CAIP-19 asset ID to add.
1382
- * @param pendingMetadata - Optional token metadata from the UI (pendingTokens format).
1383
- */
1384
- async addCustomAsset(accountId, assetId, pendingMetadata) {
1385
- const normalizedAssetId = normalizeAssetId(assetId);
1386
- log('Adding custom asset', { accountId, assetId: normalizedAssetId });
1387
- this.update((state) => {
1388
- const customAssets = state.customAssets;
1389
- if (!customAssets[accountId]) {
1390
- customAssets[accountId] = [];
1391
- }
1392
- // Only add if not already present
1393
- if (!customAssets[accountId].includes(normalizedAssetId)) {
1394
- customAssets[accountId].push(normalizedAssetId);
1395
- }
1396
- // Unhide the asset if it was hidden (via assetPreferences)
1397
- const prefs = state.assetPreferences[normalizedAssetId];
1398
- if (prefs?.hidden) {
1399
- delete prefs.hidden;
1400
- if (Object.keys(prefs).length === 0) {
1401
- delete state.assetPreferences[normalizedAssetId];
1402
- }
1403
- }
1404
- // Persist metadata from the UI so the token is immediately renderable
1405
- if (pendingMetadata) {
1406
- const parsed = parseCaipAssetType(normalizedAssetId);
1407
- let tokenType = 'erc20';
1408
- if (this.#isNativeAsset(normalizedAssetId)) {
1409
- tokenType = 'native';
1410
- }
1411
- else if (parsed.chain.namespace === KnownCaipNamespace.Solana &&
1412
- parsed.assetNamespace === CaipAssetNamespace.Token) {
1413
- tokenType = 'spl';
1414
- }
1415
- const assetMetadata = {
1416
- type: tokenType,
1417
- symbol: pendingMetadata.symbol,
1418
- name: pendingMetadata.name,
1419
- decimals: pendingMetadata.decimals,
1420
- image: pendingMetadata.iconUrl,
1421
- aggregators: pendingMetadata.aggregators,
1422
- occurrences: pendingMetadata.occurrences,
1423
- };
1424
- state.assetsInfo[normalizedAssetId] =
1425
- assetMetadata;
1426
- }
1427
- // Seed a zero balance so the UI can render the asset immediately,
1428
- // before the next pipeline fetch returns the real amount.
1429
- const balances = state.assetsBalance;
1430
- balances[accountId] ??= {};
1431
- balances[accountId][normalizedAssetId] ??= { amount: '0' };
1432
- });
1433
- // Fetch data for the newly added custom asset (merge to preserve other chains)
1434
- const account = this.#getSelectedAccounts().find((a) => a.id === accountId);
1435
- if (account) {
1436
- const chainId = extractChainId(normalizedAssetId);
1437
- await this.getAssets([account], {
1438
- chainIds: [chainId],
1439
- dataTypes: ['balance', 'metadata', 'price'],
1440
- assetTypes: ['fungible'],
1441
- forceUpdate: true,
1442
- updateMode: 'merge',
1443
- });
1444
- }
1445
- // Re-evaluate subscriptions so the supplemental RPC poll picks up the
1446
- // new customAsset on chains another data source already owns.
1447
- this.#subscribeAssets();
1448
- }
1449
- /**
1450
- * Remove a custom asset from an account.
1451
- *
1452
- * @param accountId - The account ID to remove the custom asset from.
1453
- * @param assetId - The CAIP-19 asset ID to remove.
1454
- */
1455
- removeCustomAsset(accountId, assetId) {
1456
- const normalizedAssetId = normalizeAssetId(assetId);
1457
- log('Removing custom asset', { accountId, assetId: normalizedAssetId });
1458
- this.update((state) => {
1459
- if (state.customAssets[accountId]) {
1460
- state.customAssets[accountId] = state.customAssets[accountId].filter((id) => id !== normalizedAssetId);
1461
- // Clean up empty arrays
1462
- if (state.customAssets[accountId].length === 0) {
1463
- delete state.customAssets[accountId];
1464
- }
1465
- }
1466
- });
1467
- // Re-evaluate subscriptions so the supplemental RPC poll for that chain
1468
- // is torn down when no more customAssets remain there.
1469
- this.#subscribeAssets();
1470
- }
1471
- /**
1472
- * Get all custom assets for an account.
1473
- *
1474
- * @param accountId - The account ID to get custom assets for.
1475
- * @returns Array of CAIP-19 asset IDs for the account's custom assets.
1476
- */
1477
- getCustomAssets(accountId) {
1478
- return this.state.customAssets[accountId] ?? [];
1479
- }
1480
- // ============================================================================
1481
- // HIDDEN ASSETS MANAGEMENT
1482
- // ============================================================================
1483
- /**
1484
- * Hide an asset globally.
1485
- * Hidden assets are excluded from the asset list returned by getAssets.
1486
- * The hidden state is stored in assetPreferences.
1487
- *
1488
- * @param assetId - The CAIP-19 asset ID to hide.
1489
- */
1490
- hideAsset(assetId) {
1491
- const normalizedAssetId = normalizeAssetId(assetId);
1492
- log('Hiding asset', { assetId: normalizedAssetId });
1493
- this.update((state) => {
1494
- if (!state.assetPreferences[normalizedAssetId]) {
1495
- state.assetPreferences[normalizedAssetId] = {};
1496
- }
1497
- state.assetPreferences[normalizedAssetId].hidden = true;
1498
- });
1499
- }
1500
- /**
1501
- * Unhide an asset globally.
1502
- *
1503
- * @param assetId - The CAIP-19 asset ID to unhide.
1504
- */
1505
- unhideAsset(assetId) {
1506
- const normalizedAssetId = normalizeAssetId(assetId);
1507
- log('Unhiding asset', { assetId: normalizedAssetId });
1508
- this.update((state) => {
1509
- const prefs = state.assetPreferences[normalizedAssetId];
1510
- if (prefs) {
1511
- delete prefs.hidden;
1512
- if (Object.keys(prefs).length === 0) {
1513
- delete state.assetPreferences[normalizedAssetId];
1514
- }
1515
- }
1516
- });
1517
- }
1518
- // ============================================================================
1519
- // CURRENT CURRENCY MANAGEMENT
1520
- // ============================================================================
1521
- /**
1522
- * Set the current currency.
1523
- *
1524
- * @param selectedCurrency - The ISO 4217 currency code to set.
1525
- */
1526
- setSelectedCurrency(selectedCurrency) {
1527
- const previousCurrency = this.state.selectedCurrency;
1528
- if (previousCurrency === selectedCurrency) {
1529
- return;
1530
- }
1531
- this.update((state) => {
1532
- state.selectedCurrency = selectedCurrency;
1533
- });
1534
- log('Current currency changed', {
1535
- previousCurrency,
1536
- selectedCurrency,
1537
- });
1538
- if (!this.#isBasicFunctionality()) {
1539
- return;
1540
- }
1541
- // Currency changed — old cached prices are in the wrong currency.
1542
- this.#priceDataSource.invalidatePriceCache();
1543
- this.getAssets(this.#getSelectedAccounts(), {
1544
- forceUpdate: true,
1545
- dataTypes: ['price'],
1546
- assetsForPriceUpdate: Object.values(this.state.assetsBalance).flatMap((balances) => Object.keys(balances)),
1547
- }).catch((error) => {
1548
- log('Failed to fetch asset prices after current currency change', error);
1549
- });
1550
- }
1551
- /**
1552
- * Fetch spot prices (bypassing deduper cache) for held assets that have no
1553
- * entry in `assetsPrice` yet. Used when switching or adding a network.
1554
- *
1555
- * @param accounts - Accounts whose held assets should be priced.
1556
- * @param chainIds - Chains to scope the check and fetch.
1557
- */
1558
- #fetchMissingPricesWithoutCache(accounts, chainIds) {
1559
- if (!this.#isBasicFunctionality() || accounts.length === 0) {
1560
- return;
1561
- }
1562
- const accountIds = new Set(accounts.map((account) => account.id));
1563
- const chainFilter = new Set(chainIds);
1564
- const assetsForPriceUpdate = [];
1565
- const prices = this.state.assetsPrice;
1566
- for (const [accountId, accountBalances] of Object.entries(this.state.assetsBalance)) {
1567
- if (!accountIds.has(accountId)) {
1568
- continue;
1569
- }
1570
- for (const assetId of Object.keys(accountBalances)) {
1571
- if (!chainFilter.has(assetId.split('/')[0])) {
1572
- continue;
1573
- }
1574
- const normalizedAssetId = normalizeAssetId(assetId);
1575
- if (prices[normalizedAssetId] ?? prices[assetId]) {
1576
- continue;
1577
- }
1578
- if (!isPriceableAsset(normalizedAssetId)) {
1579
- continue;
1580
- }
1581
- assetsForPriceUpdate.push(normalizedAssetId);
1582
- }
1583
- }
1584
- if (assetsForPriceUpdate.length === 0) {
1585
- return;
1586
- }
1587
- this.#priceDataSource.invalidatePriceCache();
1588
- this.getAssets(accounts, {
1589
- forceUpdate: true,
1590
- dataTypes: ['price'],
1591
- chainIds,
1592
- assetsForPriceUpdate,
1593
- }).catch((error) => {
1594
- log('Failed to fetch missing prices', { error });
1595
- });
1596
- }
1597
- // ============================================================================
1598
- // SUBSCRIPTIONS
1599
- // ============================================================================
1600
- /**
1601
- * Subscribe to price updates for all assets held by the given accounts.
1602
- * Polls PriceDataSource which fetches prices from balance state.
1603
- *
1604
- * @param accounts - Accounts to subscribe price updates for.
1605
- * @param chainIds - Chain IDs to filter prices for.
1606
- */
1607
- subscribeAssetsPrice(accounts, chainIds) {
1608
- if (!this.#isBasicFunctionality()) {
1609
- return;
1610
- }
1611
- const subscriptionKey = 'ds:PriceDataSource';
1612
- const existingSubscription = this.#activeSubscriptions.get(subscriptionKey);
1613
- const isUpdate = existingSubscription !== undefined;
1614
- const request = this.#buildDataRequest(accounts, chainIds, {
1615
- dataTypes: ['price'],
1616
- });
1617
- const subscribeReq = {
1618
- request,
1619
- subscriptionId: subscriptionKey,
1620
- isUpdate,
1621
- onAssetsUpdate: (response) => this.handleAssetsUpdate(response, 'PriceDataSource'),
1622
- getAssetsState: () => this.state,
1623
- };
1624
- this.#priceDataSource.subscribe(subscribeReq).catch(console.error);
1625
- // Track subscription
1626
- const subscription = {
1627
- chains: chainIds,
1628
- accountId: subscriptionKey,
1629
- assetTypes: ['fungible'],
1630
- dataTypes: ['price'],
1631
- unsubscribe: () => {
1632
- this.#activeSubscriptions.delete(subscriptionKey);
1633
- },
1634
- };
1635
- this.#activeSubscriptions.set(subscriptionKey, subscription);
1636
- }
1637
- /**
1638
- * Unsubscribe from price updates.
1639
- */
1640
- unsubscribeAssetsPrice() {
1641
- const subscriptionKey = 'ds:PriceDataSource';
1642
- const existingSubscription = this.#activeSubscriptions.get(subscriptionKey);
1643
- if (!existingSubscription) {
1644
- return;
1645
- }
1646
- this.#priceDataSource.unsubscribe(subscriptionKey).catch(console.error);
1647
- existingSubscription.unsubscribe();
1648
- }
1649
- // ============================================================================
1650
- // STATE MANAGEMENT
1651
- // ============================================================================
1652
- /**
1653
- * Reads the native asset map (CAIP-2 chain ID -> CAIP-19 native asset ID)
1654
- * from the QueryClient cache. Populated at construction via fetchQuery.
1655
- *
1656
- * @returns Cached map, or a stable empty object if not yet populated
1657
- * (stable identity so downstream memoized formatters can cache-hit).
1658
- */
1659
- #getNativeAssetMap() {
1660
- return (this.#queryApiClient.getCachedData(NATIVE_ASSETS_QUERY_KEY) ?? EMPTY_NATIVE_ASSET_MAP);
1661
- }
1662
- /**
1663
- * Checks whether the given CAIP-19 asset ID represents a native asset
1664
- * according to the cached native asset map, the asset namespace, and the asset reference.
1665
- *
1666
- * @param assetId - The CAIP-19 asset ID to check (case-insensitive).
1667
- * @returns True if the asset ID is a native asset.
1668
- */
1669
- #isNativeAsset(assetId) {
1670
- const parsed = parseCaipAssetType(assetId);
1671
- if (parsed.assetNamespace === 'slip44') {
1672
- return true;
1673
- }
1674
- const lower = assetId.toLowerCase();
1675
- const isInNativeAssetMap = Object.values(this.#getNativeAssetMap()).some((id) => id.toLowerCase() === lower);
1676
- if (isInNativeAssetMap) {
1677
- return true;
1678
- }
1679
- if (parsed.assetNamespace === 'erc20' &&
1680
- parsed.assetReference === ZERO_ADDRESS) {
1681
- return true;
1682
- }
1683
- return false;
1684
- }
1685
- /**
1686
- * Determines the asset type for a given CAIP-19 asset ID.
1687
- *
1688
- * @param assetId - The CAIP-19 asset ID.
1689
- * @returns The asset type: 'native', 'erc20', or 'spl'.
1690
- */
1691
- #getAssetType(assetId) {
1692
- const parsed = parseCaipAssetType(assetId);
1693
- if (this.#isNativeAsset(assetId)) {
1694
- return 'native';
1695
- }
1696
- if (parsed.chain.namespace === KnownCaipNamespace.Solana &&
1697
- parsed.assetNamespace === CaipAssetNamespace.Token) {
1698
- return 'spl';
1699
- }
1700
- return 'erc20';
1701
- }
1702
- /**
1703
- * Resolves native asset IDs (CAIP-19) for the given chains by looking them up
1704
- * in the cached native asset map.
1705
- * Chains without a registered native identifier are skipped.
1706
- *
1707
- * @param chains - The chain IDs to resolve native assets for.
1708
- * @returns Array of native asset IDs for the chains that have a registered identifier.
1709
- */
1710
- #resolveNativeAssetIds(chains) {
1711
- const nativeAssetMap = this.#getNativeAssetMap();
1712
- const ids = [];
1713
- for (const chainId of chains) {
1714
- const nativeId = nativeAssetMap[chainId];
1715
- if (nativeId) {
1716
- ids.push(nativeId);
1717
- }
1718
- }
1719
- return ids;
1720
- }
1721
- /**
1722
- * Returns native asset IDs for all enabled chains.
1723
- *
1724
- * @returns Array of native asset IDs, one per enabled chain that has a registered identifier.
1725
- */
1726
- #getNativeAssetIdsForEnabledChains() {
1727
- return this.#resolveNativeAssetIds(this.#enabledChains);
1728
- }
1729
- /**
1730
- * Returns native asset IDs for the chains that this account supports
1731
- * (account scopes ∩ enabled chains).
1732
- *
1733
- * @param account - The account (scopes determine which chains apply).
1734
- * @returns Array of native asset IDs, one per supported chain that has a registered identifier.
1735
- */
1736
- #getNativeAssetIdsForAccount(account) {
1737
- return this.#resolveNativeAssetIds(this.#getEnabledChainsForAccount(account));
1738
- }
1739
- /**
1740
- * Chains for the post-commit slow pipeline (Snap + RPC). Excludes chains the
1741
- * fast Accounts API path already handled without error so stale RPC data cannot
1742
- * overwrite fresh API zeros (e.g. after max send).
1743
- *
1744
- * @param chainIds - Chains requested by the caller.
1745
- * @param fastResponse - Response committed by the fast pipeline.
1746
- * @returns Chain IDs that still need the slow pipeline.
1747
- */
1748
- #getSlowPipelineChainIds(chainIds, fastResponse) {
1749
- const accountsApiChains = new Set(this.#accountsApiDataSource.getActiveChainsSync());
1750
- return chainIds.filter((chainId) => {
1751
- if (fastResponse.errors?.[chainId]) {
1752
- return true;
1753
- }
1754
- if (!accountsApiChains.has(chainId)) {
1755
- return true;
1756
- }
1757
- return false;
1758
- });
1759
- }
1760
- /**
1761
- * Ensures assetsBalance has a 0 balance for each native token (from
1762
- * NetworkEnablementController.nativeAssetIdentifiers) for each selected account.
1763
- * Only adds natives for chains that the account supports (correct accountId ↔ chain mapping).
1764
- */
1765
- #ensureNativeBalancesDefaultZero() {
1766
- const accounts = this.#getSelectedAccounts();
1767
- if (accounts.length === 0) {
1768
- return;
1769
- }
1770
- this.update((state) => {
1771
- const balances = state.assetsBalance;
1772
- for (const account of accounts) {
1773
- const accountId = account.id;
1774
- const nativeAssetIds = this.#getNativeAssetIdsForAccount(account);
1775
- if (nativeAssetIds.length === 0) {
1776
- continue;
1777
- }
1778
- if (!balances[accountId]) {
1779
- balances[accountId] = {};
1780
- }
1781
- for (const nativeAssetId of nativeAssetIds) {
1782
- if (!Object.prototype.hasOwnProperty.call(balances[accountId], nativeAssetId)) {
1783
- balances[accountId][nativeAssetId] = { amount: '0' };
1784
- }
1785
- }
1786
- }
1787
- });
1788
- }
1789
- /**
1790
- * Seed selected accounts with zero-balance entries for every
1791
- * controller-managed default tracked asset (e.g. mUSD on mainnet,
1792
- * Linea, Monad). Also re-asserts the metadata in `assetsInfo` so the
1793
- * defaults survive a future state migration that strips them.
1794
- *
1795
- * `chainsToSeed` lets callers narrow the work to a specific subset
1796
- * of chains — useful when reacting to a `NetworkEnablementController`
1797
- * change to seed only the chain that was just added (e.g. when the
1798
- * user finally turns on Monad). When `undefined`, every chain in the
1799
- * default tracked assets registry is seeded.
1800
- *
1801
- * Existing entries are never clobbered.
1802
- *
1803
- * @param chainsToSeed - Optional subset of CAIP-2 chain ids to seed.
1804
- * If omitted, all default tracked chains are seeded.
1805
- */
1806
- #ensureDefaultTrackedAssetsSeeded(chainsToSeed) {
1807
- const accounts = this.#getSelectedAccounts();
1808
- if (accounts.length === 0) {
1809
- return;
1810
- }
1811
- // Default tracked assets are ERC-20s on EVM chains today. Restrict
1812
- // seeding to accounts that have at least one EVM scope so we don't
1813
- // pollute non-EVM accounts. The wildcard `eip155:0` scope counts.
1814
- const evmAccounts = accounts.filter((account) => (account.scopes ?? []).some((scope) => scope.startsWith('eip155:')));
1815
- if (evmAccounts.length === 0) {
1816
- return;
1817
- }
1818
- // Gate seeding on whether the chain is currently enabled. This is
1819
- // why mUSD appears on mainnet/Linea immediately (those are normally
1820
- // enabled at startup) but only appears on Monad once the user
1821
- // turns the Monad network on.
1822
- const candidateChains = chainsToSeed ?? [...this.#enabledChains];
1823
- const targetChains = candidateChains.filter((chainId) => CHAINS_WITH_DEFAULT_TRACKED_ASSETS.has(chainId));
1824
- if (targetChains.length === 0) {
1825
- return;
1826
- }
1827
- this.update((state) => {
1828
- const balances = state.assetsBalance;
1829
- const metadata = state.assetsInfo;
1830
- for (const chainId of targetChains) {
1831
- const defaultAssetIds = DEFAULT_TRACKED_ASSETS_BY_CHAIN.get(chainId) ?? [];
1832
- for (const assetId of defaultAssetIds) {
1833
- // Re-seed metadata if state was hydrated from a prior version
1834
- // that didn't include defaults.
1835
- if (!metadata[assetId]) {
1836
- const seed = getDefaultAssetMetadata(assetId);
1837
- if (seed) {
1838
- metadata[assetId] = seed;
1839
- }
1840
- }
1841
- for (const account of evmAccounts) {
1842
- balances[account.id] ??= {};
1843
- const accountBalances = balances[account.id];
1844
- accountBalances[assetId] ??= { amount: '0' };
1845
- }
1846
- }
1847
- }
1848
- });
1849
- }
1850
- async #updateState(response) {
1851
- const normalizedResponse = normalizeResponse(response);
1852
- const mode = normalizedResponse.updateMode ?? 'merge';
1853
- const releaseLock = await this.#controllerMutex.acquire();
1854
- try {
1855
- const previousState = this.state;
1856
- const previousPrices = { ...this.state.assetsPrice };
1857
- // Use detectedAssets from response (assets without metadata)
1858
- const detectedAssets = normalizedResponse.detectedAssets ?? {};
1859
- // Track actual changes for logging
1860
- const changedBalances = [];
1861
- const changedMetadata = [];
1862
- this.update((state) => {
1863
- // Use type assertions to avoid deep type instantiation issues with Immer Draft types
1864
- const metadata = state.assetsInfo;
1865
- const balances = state.assetsBalance;
1866
- const prices = state.assetsPrice;
1867
- if (normalizedResponse.assetsInfo) {
1868
- for (const [key, value] of Object.entries(normalizedResponse.assetsInfo)) {
1869
- if (!isEqual(previousState.assetsInfo[key], value)) {
1870
- changedMetadata.push(key);
1871
- }
1872
- const existing = metadata[key];
1873
- const incoming = value;
1874
- // When the API returns no symbol or name for this token, it has no
1875
- // real knowledge of it (e.g. a user-deployed token not indexed by
1876
- // the API). Preserve richer metadata already in state (e.g. from
1877
- // pendingMetadata set by addCustomAsset) so that the correct
1878
- // decimals/symbol/name/image are not overwritten with empty values.
1879
- if (existing && !incoming.symbol && !incoming.name) {
1880
- metadata[key] = {
1881
- ...existing,
1882
- ...incoming,
1883
- symbol: existing.symbol,
1884
- name: existing.name,
1885
- decimals: existing.decimals ?? incoming.decimals,
1886
- image: existing.image ?? incoming.image,
1887
- };
1888
- }
1889
- else {
1890
- metadata[key] = value;
1891
- }
1892
- }
1893
- }
1894
- // Reconcile & self-heal stale asset "types" (e.g. erc20 -> native)
1895
- // using IDs from new response assetInfo and assetBalance
1896
- const assetsInfoAssetIdsSet = new Set([
1897
- ...Object.keys(normalizedResponse.assetsInfo ?? {}),
1898
- ...Object.values(normalizedResponse.assetsBalance ?? {}).flatMap((accountBalances) => Object.keys(accountBalances)),
1899
- ]);
1900
- for (const assetId of assetsInfoAssetIdsSet) {
1901
- const entry = metadata[assetId];
1902
- if (!entry) {
1903
- continue;
1904
- }
1905
- const correctType = this.#getAssetType(assetId);
1906
- if (entry.type !== correctType) {
1907
- metadata[assetId] = { ...entry, type: correctType };
1908
- if (!changedMetadata.includes(assetId)) {
1909
- changedMetadata.push(assetId);
1910
- }
1911
- }
1912
- }
1913
- if (normalizedResponse.assetsBalance) {
1914
- for (const [accountId, accountBalances] of Object.entries(normalizedResponse.assetsBalance)) {
1915
- const previousBalances = previousState.assetsBalance[accountId] ?? {};
1916
- const customAssetIds = state.customAssets[accountId] ?? [];
1917
- const replaceCoveredChains = mode === 'full' ||
1918
- normalizedResponse.replaceCoveredChainBalances === true;
1919
- const effective = mergeAccountBalances(previousBalances, accountBalances, customAssetIds, replaceCoveredChains);
1920
- // Ensure native tokens have an entry (0 if missing) for chains this account supports
1921
- const account = this.#getSelectedAccounts().find((a) => a.id === accountId);
1922
- const nativeAssetIdsForAccount = account
1923
- ? this.#getNativeAssetIdsForAccount(account)
1924
- : this.#getNativeAssetIdsForEnabledChains();
1925
- for (const nativeAssetId of nativeAssetIdsForAccount) {
1926
- if (!Object.prototype.hasOwnProperty.call(effective, nativeAssetId)) {
1927
- effective[nativeAssetId] = { amount: '0' };
1928
- }
1929
- }
1930
- for (const [assetId, balance] of Object.entries(effective)) {
1931
- const previousBalance = previousBalances[assetId];
1932
- // Coerce amounts (e.g. "1e-18" from a data source stringifying
1933
- // a JS Number) into a plain decimal so downstream BigInt()
1934
- // consumers don't crash. Decimals are read from the freshest
1935
- // merged metadata when available; when metadata is not yet
1936
- // known, the amount is left at its natural precision (only
1937
- // exponent form is rewritten) so balances arriving before
1938
- // their metadata aren't lossily truncated.
1939
- const assetDecimals = metadata[assetId]?.decimals;
1940
- const newAmount = normalizeAmountString(balance.amount, assetDecimals);
1941
- effective[assetId] = { ...balance, amount: newAmount };
1942
- const oldAmount = previousBalance?.amount;
1943
- const isNewDefaultNativeZero = oldAmount === undefined &&
1944
- newAmount === '0' &&
1945
- nativeAssetIdsForAccount.includes(assetId);
1946
- if (oldAmount !== newAmount && !isNewDefaultNativeZero) {
1947
- changedBalances.push({
1948
- accountId,
1949
- assetId,
1950
- oldAmount,
1951
- newAmount,
1952
- });
1953
- }
1954
- }
1955
- balances[accountId] = effective;
1956
- }
1957
- }
1958
- if (normalizedResponse.assetsPrice) {
1959
- for (const [key, value] of Object.entries(normalizedResponse.assetsPrice)) {
1960
- prices[key] = value;
1961
- }
1962
- }
1963
- });
1964
- // Emit state size trace (throttled to avoid JSON.stringify on every update)
1965
- this.#emitStateSizeTrace();
1966
- // Calculate changed prices
1967
- const changedPriceAssets = normalizedResponse.assetsPrice
1968
- ? Object.keys(normalizedResponse.assetsPrice).filter((assetId) => !isEqual(previousPrices[assetId], normalizedResponse.assetsPrice?.[assetId]))
1969
- : [];
1970
- // Log only actual changes
1971
- if (changedBalances.length > 0 ||
1972
- changedMetadata.length > 0 ||
1973
- changedPriceAssets.length > 0) {
1974
- log('State updated', {
1975
- changedBalances: changedBalances.length > 0 ? changedBalances : undefined,
1976
- changedMetadataCount: changedMetadata.length > 0 ? changedMetadata.length : undefined,
1977
- changedPricesCount: changedPriceAssets.length > 0
1978
- ? changedPriceAssets.length
1979
- : undefined,
1980
- newAssets: Object.keys(detectedAssets).length > 0
1981
- ? Object.entries(detectedAssets).map(([accountId, assets]) => ({
1982
- accountId,
1983
- assets,
1984
- }))
1985
- : undefined,
1986
- });
1987
- }
1988
- // Publish balance changed events
1989
- for (const change of changedBalances) {
1990
- this.messenger.publish('AssetsController:balanceChanged', {
1991
- accountId: change.accountId,
1992
- assetId: change.assetId,
1993
- previousAmount: change.oldAmount ?? '0',
1994
- newAmount: change.newAmount,
1995
- });
1996
- }
1997
- // Publish price changed event with full price data
1998
- if (changedPriceAssets.length > 0 && normalizedResponse.assetsPrice) {
1999
- // Build prices object with only changed prices
2000
- const changedPrices = {};
2001
- for (const assetId of changedPriceAssets) {
2002
- const price = normalizedResponse.assetsPrice[assetId];
2003
- if (price) {
2004
- changedPrices[assetId] = price;
2005
- }
2006
- }
2007
- this.messenger.publish('AssetsController:priceChanged', {
2008
- prices: changedPrices,
2009
- });
2010
- }
2011
- // Publish assets detected events
2012
- for (const [accountId, assetIds] of Object.entries(detectedAssets)) {
2013
- if (assetIds.length > 0) {
2014
- this.messenger.publish('AssetsController:assetsDetected', {
2015
- accountId,
2016
- assetIds,
2017
- });
2018
- }
2019
- }
2020
- }
2021
- finally {
2022
- releaseLock();
2023
- }
2024
- }
2025
- #getAssetsFromState(accounts, chainIds, assetTypes) {
2026
- const result = Object.create(null);
2027
- // Convert to Sets for O(1) lookups
2028
- const chainIdSet = new Set(chainIds);
2029
- const assetTypeSet = new Set(assetTypes);
2030
- for (const account of accounts) {
2031
- result[account.id] = Object.create(null);
2032
- const accountBalances = this.state.assetsBalance[account.id] ?? {};
2033
- for (const assetId of Object.keys(accountBalances)) {
2034
- const typedAssetId = assetId;
2035
- const asset = this.#getAssetFromState(account.id, typedAssetId, {
2036
- chainIdSet,
2037
- assetTypeSet,
2038
- });
2039
- if (asset) {
2040
- result[account.id][typedAssetId] = asset;
2041
- }
2042
- }
2043
- }
2044
- return result;
2045
- }
2046
- /**
2047
- * Compose a single combined `Asset` (balance + metadata + price + computed
2048
- * `fiatValue`) for an account/asset pair directly from controller state,
2049
- * applying the same filtering rules as `#getAssetsFromState`.
2050
- *
2051
- * @param accountId - The account ID (`InternalAccount.id`).
2052
- * @param assetId - The normalized CAIP-19 asset ID.
2053
- * @param filters - Optional filters applied before composing the asset.
2054
- * @param filters.chainIdSet - When provided, the asset's chain must be in this set.
2055
- * @param filters.assetTypeSet - When provided, the asset's type must be in this set.
2056
- * @returns The combined `Asset`, or `undefined` when the asset is missing a
2057
- * balance/metadata, is hidden/filtered out, or fails a provided filter.
2058
- */
2059
- #getAssetFromState(accountId, assetId, filters) {
2060
- const balance = this.state.assetsBalance[accountId]?.[assetId];
2061
- // Skip assets without a balance entry
2062
- if (!balance) {
2063
- return undefined;
2064
- }
2065
- const metadata = this.state.assetsInfo[assetId];
2066
- // Skip assets without metadata
2067
- if (!metadata) {
2068
- return undefined;
2069
- }
2070
- // Skip hidden assets (assetPreferences)
2071
- const prefs = this.state.assetPreferences[assetId];
2072
- if (prefs?.hidden) {
2073
- return undefined;
2074
- }
2075
- const assetChainId = extractChainId(assetId);
2076
- // Skip native tokens on Tempo networks
2077
- if (this.#shouldHideNativeToken(assetChainId, metadata)) {
2078
- return undefined;
2079
- }
2080
- if (filters?.chainIdSet && !filters.chainIdSet.has(assetChainId)) {
2081
- return undefined;
2082
- }
2083
- // Filter by asset type
2084
- if (filters?.assetTypeSet) {
2085
- const tokenAssetType = this.#tokenStandardToAssetType(metadata.type);
2086
- if (!filters.assetTypeSet.has(tokenAssetType)) {
2087
- return undefined;
2088
- }
2089
- }
2090
- const priceRaw = this.state.assetsPrice[assetId];
2091
- const price = priceRaw ?? {
2092
- price: 0,
2093
- lastUpdated: 0,
2094
- };
2095
- // Compute fiat value using BigNumber for precision
2096
- // Note: balance.amount is already in human-readable format (e.g., "1" for 1 ETH)
2097
- // so we do NOT divide by 10^decimals here
2098
- const balanceAmount = new BigNumberJS(balance.amount || '0');
2099
- const fiatValue = balanceAmount.multipliedBy(price.price || 0).toNumber();
2100
- return {
2101
- id: assetId,
2102
- chainId: assetChainId,
2103
- balance,
2104
- metadata,
2105
- price,
2106
- fiatValue,
2107
- };
2108
- }
2109
- /**
2110
- * Determines if a native token should be hidden on specific networks.
2111
- *
2112
- * @param chainId - The CAIP-2 chain ID (e.g., "eip155:42431").
2113
- * @param metadata - The asset metadata.
2114
- * @returns True if the token should be hidden, false otherwise.
2115
- */
2116
- #shouldHideNativeToken(chainId, metadata) {
2117
- // Check if it's a chain that should skip native tokens
2118
- if (!shouldSkipNativeForCaipChainId(chainId)) {
2119
- return false;
2120
- }
2121
- return metadata.type === 'native';
2122
- }
2123
- /**
2124
- * Maps a token standard to its corresponding asset type.
2125
- *
2126
- * @param tokenStandard - The token standard from metadata.
2127
- * @returns The corresponding asset type.
2128
- */
2129
- #tokenStandardToAssetType(tokenStandard) {
2130
- switch (tokenStandard) {
2131
- case 'native':
2132
- case 'erc20':
2133
- case 'spl':
2134
- return 'fungible';
2135
- case 'erc721':
2136
- return 'nft';
2137
- case 'erc1155':
2138
- // ERC1155 can be either fungible or non-fungible, treat as collectible
2139
- return 'collectible';
2140
- default:
2141
- // Unknown standards default to fungible
2142
- return 'fungible';
2143
- }
2144
- }
2145
- // ============================================================================
2146
- // START / STOP
2147
- // ============================================================================
2148
- /**
2149
- * Start asset tracking: subscribe to updates and fetch current balances.
2150
- * Idempotent — returns early if accounts/chains are not yet available or
2151
- * subscriptions are already active.
2152
- */
2153
- #start() {
2154
- const accounts = this.#getSelectedAccounts();
2155
- const chainIds = [...this.#enabledChains];
2156
- if (accounts.length === 0 || chainIds.length === 0) {
2157
- return;
2158
- }
2159
- if (this.#activeSubscriptions.size > 0) {
2160
- return;
2161
- }
2162
- log('Starting asset tracking', {
2163
- selectedAccountCount: accounts.length,
2164
- enabledChainCount: chainIds.length,
2165
- });
2166
- this.#lastKnownAccountIds = new Set(accounts.map((a) => a.id));
2167
- this.#runStartupRefresh(accounts).catch((error) => {
2168
- log('Failed to start asset tracking', error);
2169
- });
2170
- }
2171
- /**
2172
- * Stop asset tracking: unsubscribe from all updates.
2173
- * Called when app closes or keyring locks.
2174
- */
2175
- #stop() {
2176
- log('Stopping asset tracking', {
2177
- activeSubscriptionCount: this.#activeSubscriptions.size,
2178
- hasPriceSubscription: this.#activeSubscriptions.has('ds:PriceDataSource'),
2179
- });
2180
- this.#firstInitFetchReported = false;
2181
- this.#stateSizeReported = false;
2182
- this.#lastKnownAccountIds = new Set();
2183
- // Stop price subscription first (uses direct messenger call)
2184
- this.unsubscribeAssetsPrice();
2185
- // Stop balance subscriptions by properly notifying data sources via messenger
2186
- // This ensures data sources stop their polling timers.
2187
- // Use #allBalanceDataSources + staked balance source so we unsubscribe from
2188
- // every source that may have been subscribed.
2189
- const allSources = [
2190
- ...this.#allBalanceDataSources,
2191
- this.#stakedBalanceDataSource,
2192
- ];
2193
- const subscriptionKeys = [...this.#activeSubscriptions.keys()];
2194
- for (const subscriptionKey of subscriptionKeys) {
2195
- if (!subscriptionKey.startsWith('ds:')) {
2196
- continue;
2197
- }
2198
- // Subscription keys take the form `ds:<SourceName>` for the regular
2199
- // subscription or `ds:<SourceName>:<suffix>` for supplemental
2200
- // subscriptions (e.g. `ds:RpcDataSource:custom`). Split on `:` and
2201
- // pick the source-name segment so both shapes resolve correctly.
2202
- const [, sourceId] = subscriptionKey.split(':');
2203
- const source = allSources.find((ds) => ds.getName() === sourceId);
2204
- if (source) {
2205
- // Unsubscribe by the actual key — `#unsubscribeDataSource` only
2206
- // knows the regular `ds:<SourceName>` shape and would miss
2207
- // supplemental subscriptions, leaking their polling timers.
2208
- this.#unsubscribeBySubscriptionKey(source, subscriptionKey);
2209
- }
2210
- }
2211
- this.#activeSubscriptions.clear();
2212
- }
2213
- /**
2214
- * Handle basic functionality toggle change. Call this from the consumer (extension or mobile)
2215
- * when the user changes the "Basic functionality" setting. Refreshes subscriptions so the
2216
- * current {@link AssetsControllerOptions.isBasicFunctionality} getter is used (true = APIs on,
2217
- * false = RPC only).
2218
- *
2219
- * @param _isBasic - The new value (for call-site clarity; the getter is the source of truth).
2220
- */
2221
- handleBasicFunctionalityChange(_isBasic) {
2222
- this.#stop();
2223
- this.#subscribeAssets();
2224
- }
2225
- /**
2226
- * Subscribe to asset updates for all selected accounts.
2227
- *
2228
- * @param options - Subscription options.
2229
- * @param options.skipInitialFetch - When true, AccountsApi skips its
2230
- * one-shot subscribe poll (use after a force `getAssets` for the same scope).
2231
- */
2232
- #subscribeAssets(options) {
2233
- const accounts = this.#getSelectedAccounts();
2234
- const enabledChains = [...this.#enabledChains];
2235
- if (accounts.length === 0 || enabledChains.length === 0) {
2236
- return;
2237
- }
2238
- // Subscribe to balance updates (batched by data source)
2239
- this.#subscribeAssetsBalance(accounts, enabledChains, options);
2240
- // Subscribe to staked balance updates (separate from regular balance chain-claiming)
2241
- this.#subscribeStakedBalance(accounts, enabledChains);
2242
- // Subscribe to price updates for all assets held by selected accounts
2243
- this.subscribeAssetsPrice(accounts, enabledChains);
2244
- }
2245
- /**
2246
- * Subscribe to balance updates for the given accounts and chains.
2247
- *
2248
- * Strategy to minimize data source calls:
2249
- * 1. Collect all chains to subscribe based on enabled networks
2250
- * 2. Map chains to accounts based on their scopes
2251
- * 3. Split by data source (ordered by priority) - each data source gets ONE subscription
2252
- *
2253
- * This ensures we make minimal subscriptions to each data source while covering
2254
- * all accounts and chains.
2255
- *
2256
- * @param accounts - Accounts to subscribe balance updates for.
2257
- * @param chainIds - Chain IDs to subscribe for.
2258
- * @param options - Subscription options.
2259
- * @param options.skipInitialFetch - Forwarded to AccountsApi subscribe.
2260
- */
2261
- #subscribeAssetsBalance(accounts, chainIds, options) {
2262
- const chainToAccounts = this.#buildChainToAccountsMap(accounts, new Set(chainIds));
2263
- const remainingChains = new Set(chainToAccounts.keys());
2264
- // When basic functionality is on, use all balance data sources; when off,
2265
- // RPC only.
2266
- const isBasicFunctionality = this.#isBasicFunctionality();
2267
- const balanceDataSources = isBasicFunctionality
2268
- ? this.#allBalanceDataSources
2269
- : [this.#rpcDataSource];
2270
- let rpcAssignedChains = new Set();
2271
- for (const source of balanceDataSources) {
2272
- const availableChains = new Set(source.getActiveChainsSync());
2273
- const assignedChains = [];
2274
- for (const chainId of remainingChains) {
2275
- if (availableChains.has(chainId)) {
2276
- assignedChains.push(chainId);
2277
- remainingChains.delete(chainId);
2278
- }
2279
- }
2280
- if (assignedChains.length === 0) {
2281
- this.#unsubscribeDataSource(source);
2282
- continue;
2283
- }
2284
- if (source === this.#rpcDataSource) {
2285
- rpcAssignedChains = new Set(assignedChains);
2286
- }
2287
- const seenIds = new Set();
2288
- const accountsForSource = assignedChains
2289
- .flatMap((chainId) => chainToAccounts.get(chainId) ?? [])
2290
- .filter((account) => {
2291
- if (seenIds.has(account.id)) {
2292
- return false;
2293
- }
2294
- seenIds.add(account.id);
2295
- return true;
2296
- });
2297
- if (accountsForSource.length > 0) {
2298
- this.#subscribeDataSource(source, accountsForSource, assignedChains, {
2299
- ...(options?.skipInitialFetch &&
2300
- source === this.#accountsApiDataSource
2301
- ? { skipInitialFetch: true }
2302
- : {}),
2303
- });
2304
- }
2305
- }
2306
- // Supplemental RPC subscription for customAssets on chains another data
2307
- // source claimed during regular handoff. RPC is the sole balance fetcher
2308
- // for customAssets, so we must always poll them — even when (e.g.)
2309
- // AccountsApi is already covering the chain for normal balances. The
2310
- // supplemental subscription runs in `customAssetsOnly` mode so it does
2311
- // NOT double-poll the regular tracked balances.
2312
- this.#subscribeRpcCustomAssetsSupplement(accounts, chainToAccounts, rpcAssignedChains);
2313
- }
2314
- /**
2315
- * Guarantee that customAssets are **always** polled by RPC, even when
2316
- * AccountsApi or another data source has claimed the chain in the
2317
- * regular handoff. RPC is the sole balance fetcher for user-imported
2318
- * tokens (see `pickRpcCustomAssetsSupplement` for the full rationale),
2319
- * so we run a dedicated subscription in `customAssetsOnly` mode under a
2320
- * distinct subscription key (`ds:RpcDataSource:custom`) that does not
2321
- * interfere with the regular RPC subscription.
2322
- *
2323
- * @param accounts - Accounts to consider for customAssets.
2324
- * @param chainToAccounts - Map of chain → accounts (built by caller).
2325
- * @param rpcAssignedChains - Chains RPC was assigned in the regular handoff.
2326
- */
2327
- #subscribeRpcCustomAssetsSupplement(accounts, chainToAccounts, rpcAssignedChains) {
2328
- const rpc = this.#rpcDataSource;
2329
- const supplementalKey = `ds:${rpc.getName()}:custom`;
2330
- const decision = pickRpcCustomAssetsSupplement({
2331
- accountIds: accounts.map((account) => account.id),
2332
- customAssetsByAccount: this.state.customAssets,
2333
- rpcAssignedChains,
2334
- rpcAvailableChains: new Set(rpc.getActiveChainsSync()),
2335
- enabledChains: new Set(chainToAccounts.keys()),
2336
- });
2337
- if (decision.chains.length === 0) {
2338
- this.#unsubscribeBySubscriptionKey(rpc, supplementalKey);
2339
- return;
2340
- }
2341
- const supplementalAccounts = accounts.filter((account) => decision.accountIds.has(account.id));
2342
- if (supplementalAccounts.length === 0) {
2343
- this.#unsubscribeBySubscriptionKey(rpc, supplementalKey);
2344
- return;
2345
- }
2346
- this.#subscribeDataSource(rpc, supplementalAccounts, decision.chains, {
2347
- subscriptionKey: supplementalKey,
2348
- customAssetsOnly: true,
2349
- });
2350
- }
2351
- /**
2352
- * Unsubscribe a data source by an explicit subscription key.
2353
- *
2354
- * @param source - The data source instance.
2355
- * @param subscriptionKey - The subscription key to unsubscribe.
2356
- */
2357
- #unsubscribeBySubscriptionKey(source, subscriptionKey) {
2358
- const existingSubscription = this.#activeSubscriptions.get(subscriptionKey);
2359
- if (existingSubscription) {
2360
- source.unsubscribe(subscriptionKey).catch(() => undefined);
2361
- existingSubscription.unsubscribe();
2362
- }
2363
- }
2364
- /**
2365
- * Subscribe to staked balance updates.
2366
- * Unlike regular balance data sources, the staked balance data source provides
2367
- * supplementary data and does not participate in chain-claiming.
2368
- *
2369
- * @param accounts - Accounts to subscribe staked balance updates for.
2370
- * @param chainIds - Chain IDs to subscribe for.
2371
- */
2372
- #subscribeStakedBalance(accounts, chainIds) {
2373
- const source = this.#stakedBalanceDataSource;
2374
- if (!source) {
2375
- return;
2376
- }
2377
- const availableChains = new Set(source.getActiveChainsSync());
2378
- const assignedChains = chainIds.filter((chainId) => availableChains.has(chainId));
2379
- if (assignedChains.length === 0) {
2380
- this.#unsubscribeDataSource(source);
2381
- return;
2382
- }
2383
- this.#subscribeDataSource(source, accounts, assignedChains);
2384
- }
2385
- /**
2386
- * Build a mapping of chainId -> accounts that support that chain.
2387
- * Only includes chains that are in the chainsToSubscribe set.
2388
- *
2389
- * @param accounts - Array of accounts to build mapping for.
2390
- * @param chainsToSubscribe - Set of chain IDs to include in the mapping.
2391
- * @returns Map of chainId to array of accounts that support that chain.
2392
- */
2393
- #buildChainToAccountsMap(accounts, chainsToSubscribe) {
2394
- const chainToAccounts = new Map();
2395
- for (const account of accounts) {
2396
- for (const chainId of this.#getEnabledChainsForAccount(account)) {
2397
- if (!chainsToSubscribe.has(chainId)) {
2398
- continue;
2399
- }
2400
- let list = chainToAccounts.get(chainId);
2401
- if (!list) {
2402
- list = [];
2403
- chainToAccounts.set(chainId, list);
2404
- }
2405
- list.push(account);
2406
- }
2407
- }
2408
- return chainToAccounts;
2409
- }
2410
- /**
2411
- * Subscribe to a specific data source with accounts and chains.
2412
- * Uses the data source name as the subscription key for batching.
2413
- *
2414
- * @param source - The balance data source instance.
2415
- * @param accounts - Array of accounts to subscribe for.
2416
- * @param chains - Array of chain IDs to subscribe for.
2417
- * @param options - Optional subscription overrides.
2418
- * @param options.subscriptionKey - Custom subscription key (default: `ds:<sourceId>`).
2419
- * @param options.customAssetsOnly - When true, only poll customAssets for these chains.
2420
- * @param options.skipInitialFetch - When true, skip the data source's subscribe-time fetch.
2421
- */
2422
- #subscribeDataSource(source, accounts, chains, options = {}) {
2423
- const sourceId = source.getName();
2424
- const subscriptionKey = options.subscriptionKey ?? `ds:${sourceId}`;
2425
- const existingSubscription = this.#activeSubscriptions.get(subscriptionKey);
2426
- const isUpdate = existingSubscription !== undefined;
2427
- log('Subscribe to data source', {
2428
- sourceId,
2429
- subscriptionKey,
2430
- isUpdate,
2431
- accountCount: accounts.length,
2432
- chainCount: chains.length,
2433
- customAssetsOnly: options.customAssetsOnly === true,
2434
- skipInitialFetch: options.skipInitialFetch === true,
2435
- });
2436
- const subscribeReq = {
2437
- request: this.#buildDataRequest(accounts, chains, {
2438
- assetTypes: ['fungible'],
2439
- dataTypes: ['balance'],
2440
- updateInterval: this.#defaultUpdateInterval,
2441
- ...(options.customAssetsOnly === true
2442
- ? { customAssetsOnly: true }
2443
- : {}),
2444
- }),
2445
- subscriptionId: subscriptionKey,
2446
- isUpdate,
2447
- onAssetsUpdate: (response, request) => this.handleAssetsUpdate(response, sourceId, request),
2448
- getAssetsState: () => this.state,
2449
- ...(options.skipInitialFetch === true ? { skipInitialFetch: true } : {}),
2450
- };
2451
- source.subscribe(subscribeReq).catch((error) => {
2452
- console.error(`[AssetsController] Failed to subscribe to '${sourceId}':`, error);
2453
- emitTrace({
2454
- name: TRACE_SUBSCRIPTION_ERROR,
2455
- trace: this.#trace,
2456
- data: {
2457
- source: sourceId,
2458
- error_message: String(error),
2459
- },
2460
- });
2461
- });
2462
- // Track subscription
2463
- const subscription = {
2464
- chains,
2465
- accountId: subscriptionKey,
2466
- assetTypes: ['fungible'],
2467
- dataTypes: ['balance'],
2468
- unsubscribe: () => {
2469
- this.#activeSubscriptions.delete(subscriptionKey);
2470
- },
2471
- };
2472
- this.#activeSubscriptions.set(subscriptionKey, subscription);
2473
- }
2474
- /**
2475
- * Unsubscribe from a data source if we have an active subscription.
2476
- *
2477
- * @param source - The balance data source instance to unsubscribe from.
2478
- */
2479
- #unsubscribeDataSource(source) {
2480
- const subscriptionKey = `ds:${source.getName()}`;
2481
- const existingSubscription = this.#activeSubscriptions.get(subscriptionKey);
2482
- if (existingSubscription) {
2483
- source.unsubscribe(subscriptionKey).catch(() => undefined);
2484
- existingSubscription.unsubscribe();
2485
- }
2486
- }
2487
- // ============================================================================
2488
- // HELPERS
2489
- // ============================================================================
2490
- /**
2491
- * Build a DataRequest with accountsWithSupportedChains (enabled chains ∩ account scope ∩ requested chainIds).
2492
- *
2493
- * @param accounts - Accounts to include.
2494
- * @param chainIds - Requested chain IDs (e.g. enabled chains or subset).
2495
- * @param partial - Rest of the request (dataTypes, assetTypes, etc.).
2496
- * @returns DataRequest with accountsWithSupportedChains and chainIds.
2497
- */
2498
- #buildDataRequest(accounts, chainIds, partial = {}) {
2499
- const chainIdSet = new Set(chainIds);
2500
- // Force update will pass in additional chains for wildcard account scope
2501
- // Enables updates for chains that are not selected (e.g. for unapproved transactions)
2502
- const forceUpdateChains = partial.forceUpdate ? chainIds : undefined;
2503
- const accountsWithSupportedChains = accounts.map((account) => ({
2504
- account,
2505
- supportedChains: this.#getEnabledChainsForAccount(account, forceUpdateChains).filter((chain) => chainIdSet.has(chain)),
2506
- }));
2507
- return {
2508
- accountsWithSupportedChains,
2509
- chainIds,
2510
- ...partial,
2511
- };
2512
- }
2513
- /**
2514
- * Get the chains that an account supports based on its scopes.
2515
- * Returns the intersection of the account's scopes and the enabled chains.
2516
- *
2517
- * @param account - The account to get supported chains for.
2518
- * @param additionalChains - Optional extra chains to include for wildcard scopes.
2519
- * @returns Array of ChainIds that the account supports and are enabled.
2520
- */
2521
- #getEnabledChainsForAccount(account, additionalChains) {
2522
- // Account scopes are CAIP-2 chain IDs like "eip155:1", "solana:mainnet", "bip122:..."
2523
- const scopes = account.scopes ?? [];
2524
- const result = [];
2525
- for (const scope of scopes) {
2526
- const scopeStr = scope;
2527
- if (!isCaipChainId(scopeStr)) {
2528
- result.push(scope);
2529
- continue;
2530
- }
2531
- const { namespace, reference } = parseCaipChainId(scopeStr);
2532
- // Wildcard scope (e.g., "eip155:0" means all enabled chains in that namespace)
2533
- if (reference === '0') {
2534
- // Add enabled chains
2535
- for (const chain of this.#enabledChains) {
2536
- if (isCaipChainId(chain)) {
2537
- const chainParsed = parseCaipChainId(chain);
2538
- if (chainParsed.namespace === namespace) {
2539
- result.push(chain);
2540
- }
2541
- }
2542
- }
2543
- // Add additional chains
2544
- for (const chain of additionalChains ?? []) {
2545
- if (isCaipChainId(chain)) {
2546
- const chainParsed = parseCaipChainId(chain);
2547
- if (chainParsed.namespace === namespace) {
2548
- result.push(chain);
2549
- }
2550
- }
2551
- }
2552
- }
2553
- else if (namespace === 'eip155' && isStrictHexString(reference)) {
2554
- // Normalize hex to decimal for EIP155
2555
- result.push(`eip155:${parseInt(reference, 16)}`);
2556
- }
2557
- else {
2558
- result.push(scope);
2559
- }
2560
- }
2561
- return result;
2562
- }
2563
- // ============================================================================
2564
- // EVENT HANDLERS
2565
- // ============================================================================
2566
- async #handleAccountGroupChanged(groupId, previousGroupId = '') {
2567
- // The selected account group can be empty during onboarding or wallet reset.
2568
- if (!groupId) {
2569
- return;
2570
- }
2571
- // First start is owned by `AccountTreeController:initialized` / `#start`.
2572
- // ATC also re-publishes `selectedAccountGroupChange` on every `init()`
2573
- // (including when the group did not change) so late subscribers can catch
2574
- // up — ignore those until we are already tracking.
2575
- if (this.#activeSubscriptions.size === 0) {
2576
- return;
2577
- }
2578
- if (groupId === previousGroupId) {
2579
- return;
2580
- }
2581
- const accounts = this.#getSelectedAccounts();
2582
- log('Account group changed', {
2583
- accountCount: accounts.length,
2584
- accountIds: accounts.map((a) => a.id),
2585
- groupId,
2586
- previousGroupId,
2587
- });
2588
- this.#lastKnownAccountIds = new Set(accounts.map((a) => a.id));
2589
- const releaseLock = await this.#accountRefreshMutex.acquire();
2590
- try {
2591
- if (accounts.length > 0) {
2592
- await this.getAssets(accounts, {
2593
- chainIds: [...this.#enabledChains],
2594
- forceUpdate: true,
2595
- });
2596
- }
2597
- this.#ensureNativeBalancesDefaultZero();
2598
- this.#ensureDefaultTrackedAssetsSeeded();
2599
- // Subscribe after seed so the price poll sees natives / defaults.
2600
- // Balances were just force-fetched — skip AccountsApi's subscribe-time poll.
2601
- this.#subscribeAssets({ skipInitialFetch: true });
2602
- this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]);
2603
- }
2604
- finally {
2605
- releaseLock();
2606
- }
2607
- }
2608
- async #handleEnabledNetworksChanged(enabledNetworkMap) {
2609
- const previousChains = this.#enabledChains;
2610
- this.#enabledChains = this.#extractEnabledChains(enabledNetworkMap);
2611
- // Find newly enabled chains (in new set but not in previous)
2612
- const addedChains = [];
2613
- for (const chain of this.#enabledChains) {
2614
- if (!previousChains.has(chain)) {
2615
- addedChains.push(chain);
2616
- }
2617
- }
2618
- // Find disabled chains to clean up (in previous but not in new)
2619
- const removedChains = [];
2620
- for (const chain of previousChains) {
2621
- if (!this.#enabledChains.has(chain)) {
2622
- removedChains.push(chain);
2623
- }
2624
- }
2625
- log('Enabled networks changed', {
2626
- previousCount: previousChains.size,
2627
- newCount: this.#enabledChains.size,
2628
- addedChains,
2629
- removedChains,
2630
- });
2631
- // Note: We intentionally do NOT delete balance data for disabled chains.
2632
- // Users may want to see historical balances even if the network is currently disabled.
2633
- // The data will simply not be updated until the network is re-enabled.
2634
- // Refresh subscriptions for new chain set
2635
- this.#subscribeAssets();
2636
- // Do one-time fetch for newly enabled chains; merge so we keep existing chain balances
2637
- const accounts = this.#getSelectedAccounts();
2638
- if (addedChains.length > 0 && accounts.length > 0) {
2639
- await this.getAssets(accounts, {
2640
- chainIds: addedChains,
2641
- forceUpdate: true,
2642
- updateMode: 'merge',
2643
- });
2644
- }
2645
- this.#ensureNativeBalancesDefaultZero();
2646
- // Seed default tracked assets (mUSD) for any chain the user has
2647
- // *just* enabled. This is what makes mUSD appear on Monad after
2648
- // the user finally adds it to NetworkEnablementController.
2649
- if (addedChains.length > 0) {
2650
- this.#ensureDefaultTrackedAssetsSeeded(addedChains);
2651
- }
2652
- this.#fetchMissingPricesWithoutCache(accounts, [...this.#enabledChains]);
2653
- }
2654
- /**
2655
- * Handle a `NetworkController:networkAdded` event. When the user
2656
- * adds a network (e.g. Monad) to NetworkController, seed the
2657
- * controller-managed default tracked assets for that chain (mUSD on
2658
- * Monad) into state immediately — without waiting for the chain to
2659
- * also be enabled in NetworkEnablementController. No-op for chains
2660
- * that aren't in the defaults registry.
2661
- *
2662
- * @param hexChainId - Hex chain id of the newly-added network
2663
- * configuration (e.g. `0x279f`).
2664
- */
2665
- #handleNetworkAdded(hexChainId) {
2666
- let caipChainId;
2667
- try {
2668
- caipChainId = `eip155:${parseInt(hexChainId, 16)}`;
2669
- }
2670
- catch {
2671
- return;
2672
- }
2673
- if (!CHAINS_WITH_DEFAULT_TRACKED_ASSETS.has(caipChainId)) {
2674
- return;
2675
- }
2676
- log('Network added — seeding default tracked assets', {
2677
- hexChainId,
2678
- caipChainId,
2679
- });
2680
- this.#ensureDefaultTrackedAssetsSeeded([caipChainId]);
2681
- const accounts = this.#getSelectedAccounts();
2682
- this.#fetchMissingPricesWithoutCache(accounts, [caipChainId]);
2683
- }
2684
- /**
2685
- * Refresh data-source `activeChains` after an EVM network switch so API/Rpc
2686
- * chain claiming is not stuck on an empty or stale init-time list.
2687
- */
2688
- async #refreshActiveChainsOnNetworkSwitch() {
2689
- await this.#accountsApiDataSource.refreshActiveChains();
2690
- this.#rpcDataSource.refreshActiveChainsFromNetworkState();
2691
- }
2692
- /**
2693
- * Resolve the CAIP-2 chain ID for the currently selected EVM network client.
2694
- *
2695
- * @param networkState - NetworkController state from `networkDidChange`.
2696
- * @returns CAIP-2 chain ID (e.g. `eip155:1`) or undefined when unavailable.
2697
- */
2698
- #getSelectedEvmChainIdFromNetworkState(networkState) {
2699
- try {
2700
- const networkClient = this.messenger.call('NetworkController:getNetworkClientById', networkState.selectedNetworkClientId);
2701
- const hexChainId = networkClient.configuration?.chainId;
2702
- if (!hexChainId) {
2703
- return undefined;
2704
- }
2705
- return `eip155:${parseInt(hexChainId, 16)}`;
2706
- }
2707
- catch {
2708
- return undefined;
2709
- }
2710
- }
2711
- /**
2712
- * Refresh subscriptions and fetch balances when the user switches the
2713
- * selected EVM network in the UI (`NetworkController:networkDidChange`).
2714
- *
2715
- * @param networkState - NetworkController state after the switch.
2716
- */
2717
- async #handleNetworkDidChange(networkState) {
2718
- if (!this.#uiOpen || !this.#keyringUnlocked || !this.#isEnabled()) {
2719
- return;
2720
- }
2721
- const accounts = this.#getSelectedAccounts();
2722
- if (accounts.length === 0) {
2723
- return;
2724
- }
2725
- const selectedChainId = this.#getSelectedEvmChainIdFromNetworkState(networkState);
2726
- if (!selectedChainId) {
2727
- return;
2728
- }
2729
- log('Selected EVM network switched', {
2730
- selectedNetworkClientId: networkState.selectedNetworkClientId,
2731
- selectedChainId,
2732
- });
2733
- const releaseLock = await this.#accountRefreshMutex.acquire();
2734
- try {
2735
- await this.#refreshActiveChainsOnNetworkSwitch();
2736
- this.#subscribeAssets();
2737
- await this.getAssets(accounts, {
2738
- chainIds: [selectedChainId],
2739
- forceUpdate: true,
2740
- dataTypes: ['balance', 'metadata', 'price'],
2741
- });
2742
- this.#ensureNativeBalancesDefaultZero();
2743
- this.#fetchMissingPricesWithoutCache(accounts, [selectedChainId]);
2744
- }
2745
- finally {
2746
- releaseLock();
2747
- }
2748
- }
2749
- /**
2750
- * Refresh balances after a network configuration is removed.
2751
- */
2752
- #refreshAssetsAfterNetworkChange() {
2753
- const accounts = this.#getSelectedAccounts();
2754
- if (accounts.length === 0) {
2755
- return;
2756
- }
2757
- this.getAssets(accounts, {
2758
- forceUpdate: true,
2759
- dataTypes: ['balance', 'metadata'],
2760
- }).catch((error) => {
2761
- log('Failed to refresh assets after network change', { error });
2762
- });
2763
- }
2764
- /**
2765
- * Refresh balances and fetch missing prices after a network is added.
2766
- *
2767
- * @param hexChainId - Hex chain id of the newly-added network.
2768
- */
2769
- async #refreshAssetsAfterNetworkAdded(hexChainId) {
2770
- const accounts = this.#getSelectedAccounts();
2771
- if (accounts.length === 0) {
2772
- return;
2773
- }
2774
- let caipChainId;
2775
- try {
2776
- caipChainId = `eip155:${parseInt(hexChainId, 16)}`;
2777
- }
2778
- catch {
2779
- return;
2780
- }
2781
- await this.getAssets(accounts, {
2782
- chainIds: [caipChainId],
2783
- forceUpdate: true,
2784
- dataTypes: ['balance', 'metadata', 'price'],
2785
- });
2786
- this.#ensureNativeBalancesDefaultZero();
2787
- this.#fetchMissingPricesWithoutCache(accounts, [caipChainId]);
2788
- }
2789
- /**
2790
- * Handle assets updated from a data source.
2791
- * Called via the onAssetsUpdate callback passed in SubscriptionRequest when the controller subscribes to a data source.
2792
- * Runs detection, then (when basic functionality is enabled) token metadata and price enrichment before updating state.
2793
- * When basic functionality is disabled (RPC-only mode), only detection runs; token and price APIs are not used.
2794
- *
2795
- * @param response - The data response with updated assets
2796
- * @param sourceId - The data source ID reporting the update
2797
- * @param request - Optional original request for context when enriching
2798
- */
2799
- async handleAssetsUpdate(response, sourceId, request) {
2800
- log('Assets updated from data source', {
2801
- sourceId,
2802
- hasBalance: Boolean(response.assetsBalance),
2803
- hasPrice: Boolean(response.assetsPrice),
2804
- });
2805
- // Enrichment spans only before unlock/first-init fetch completes.
2806
- const pipelineTrace = this.#firstInitFetchReported
2807
- ? undefined
2808
- : this.#trace;
2809
- await withTrace({
2810
- name: TRACE_UPDATE_PARENT,
2811
- trace: pipelineTrace,
2812
- data: {
2813
- source: sourceId,
2814
- has_balance: Boolean(response.assetsBalance),
2815
- has_price: Boolean(response.assetsPrice),
2816
- balance_account_count: response.assetsBalance
2817
- ? Object.keys(response.assetsBalance).length
2818
- : 0,
2819
- },
2820
- fn: async (parentContext) => {
2821
- const updateStart = performance.now();
2822
- const resolvedRequest = request ?? {
2823
- accountsWithSupportedChains: [],
2824
- chainIds: [],
2825
- dataTypes: ['balance', 'metadata', 'price'],
2826
- };
2827
- // RPC-only mode (basic functionality off): never run token/price APIs. Strip
2828
- // those data types so downstream middleware cannot treat them as requested.
2829
- const pipelineRequest = this.#isBasicFunctionality()
2830
- ? resolvedRequest
2831
- : {
2832
- ...resolvedRequest,
2833
- dataTypes: resolvedRequest.dataTypes.filter((dt) => dt !== 'metadata' && dt !== 'price'),
2834
- };
2835
- // Graduate custom assets only when AccountsAPI / AccountActivity reports
2836
- // them. RPC already fetches custom assets on purpose, and Snap handles
2837
- // non-EVM chains the rule does not apply to, so skip the middleware for
2838
- // those.
2839
- const shouldGraduateCustomAssets = sourceId === 'AccountsApiDataSource' ||
2840
- sourceId === 'AccountActivityDataSource';
2841
- // Websocket updates can carry brand-new spam airdrops: enrich them
2842
- // with Token API occurrences and drop below-floor tokens BEFORE
2843
- // detection, so spam is never detected, enriched, priced or persisted.
2844
- const shouldFilterOccurrences = sourceId === 'AccountActivityDataSource' &&
2845
- this.#isBasicFunctionality();
2846
- const enrichmentSources = [
2847
- ...(shouldGraduateCustomAssets
2848
- ? [this.#customAssetGraduationMiddleware]
2849
- : []),
2850
- ...(shouldFilterOccurrences
2851
- ? [
2852
- {
2853
- getName: () => 'OccurrenceFloorFilter',
2854
- assetsMiddleware: this.#tokenDataSource.occurrenceFilterMiddleware,
2855
- },
2856
- ]
2857
- : []),
2858
- this.#detectionMiddleware,
2859
- ];
2860
- if (this.#isBasicFunctionality()) {
2861
- enrichmentSources.push(createParallelMiddleware([
2862
- this.#tokenDataSource,
2863
- this.#priceDataSource,
2864
- ]));
2865
- }
2866
- const { response: enrichedResponse } = await this.#executeMiddlewares({
2867
- sources: enrichmentSources,
2868
- request: pipelineRequest,
2869
- initialResponse: response,
2870
- parentContext,
2871
- trace: pipelineTrace,
2872
- });
2873
- await this.#updateState({
2874
- ...enrichedResponse,
2875
- replaceCoveredChainBalances: response.replaceCoveredChainBalances,
2876
- });
2877
- // Summary fields for Assets Health (nested under the parent span).
2878
- emitTrace({
2879
- name: TRACE_UPDATE_PIPELINE,
2880
- trace: pipelineTrace,
2881
- data: {
2882
- source: sourceId,
2883
- duration_ms: performance.now() - updateStart,
2884
- has_balance: Boolean(response.assetsBalance),
2885
- has_price: Boolean(response.assetsPrice),
2886
- has_metadata: Boolean(enrichedResponse.assetsInfo),
2887
- balance_account_count: response.assetsBalance
2888
- ? Object.keys(response.assetsBalance).length
2889
- : 0,
2890
- },
2891
- parentContext,
2892
- });
2893
- },
2894
- });
2895
- }
2896
- // ============================================================================
2897
- // CLEANUP
2898
- // ============================================================================
2899
- destroy() {
2900
- log('Destroying AssetsController', {
2901
- dataSourceCount: this.#allBalanceDataSources.length,
2902
- subscriptionCount: this.#activeSubscriptions.size,
2903
- });
2904
- // Destroy instantiated data sources
2905
- this.#accountActivityDataSource?.destroy?.();
2906
- this.#accountsApiDataSource?.destroy?.();
2907
- this.#snapDataSource?.destroy?.();
2908
- this.#rpcDataSource?.destroy?.();
2909
- this.#stakedBalanceDataSource?.destroy?.();
2910
- // Stop all active subscriptions
2911
- this.#stop();
2912
- if (this.#unsubscribeBasicFunctionality) {
2913
- this.#unsubscribeBasicFunctionality();
2914
- this.#unsubscribeBasicFunctionality = null;
2915
- }
2916
- // Unregister action handlers
2917
- this.messenger.unregisterActionHandler('AssetsController:getAssets');
2918
- this.messenger.unregisterActionHandler('AssetsController:getAssetsBalance');
2919
- this.messenger.unregisterActionHandler('AssetsController:getAssetMetadata');
2920
- this.messenger.unregisterActionHandler('AssetsController:getAccountAssetByID');
2921
- this.messenger.unregisterActionHandler('AssetsController:getAccountAssetsByIDs');
2922
- this.messenger.unregisterActionHandler('AssetsController:getAccountAssetsByScope');
2923
- this.messenger.unregisterActionHandler('AssetsController:getAssetsPrice');
2924
- this.messenger.unregisterActionHandler('AssetsController:getExchangeRatesForBridge');
2925
- this.messenger.unregisterActionHandler('AssetsController:getStateForTransactionPay');
2926
- this.messenger.unregisterActionHandler('AssetsController:addCustomAsset');
2927
- this.messenger.unregisterActionHandler('AssetsController:removeCustomAsset');
2928
- this.messenger.unregisterActionHandler('AssetsController:getCustomAssets');
2929
- this.messenger.unregisterActionHandler('AssetsController:hideAsset');
2930
- this.messenger.unregisterActionHandler('AssetsController:unhideAsset');
2931
- this.messenger.unregisterActionHandler('AssetsController:setSelectedCurrency');
2932
- }
2933
- }
2934
- //# sourceMappingURL=AssetsController.js.map