tapimo 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/dist/App.d.ts +6396 -4575
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +39 -4
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.d.ts +15 -0
  6. package/dist/Assets.d.ts.map +1 -1
  7. package/dist/Assets.js +7 -0
  8. package/dist/Assets.js.map +1 -1
  9. package/dist/Scope.d.ts +2 -0
  10. package/dist/Scope.d.ts.map +1 -1
  11. package/dist/Scope.js +17 -0
  12. package/dist/Scope.js.map +1 -1
  13. package/dist/admin/App.d.ts +176 -78
  14. package/dist/admin/App.d.ts.map +1 -1
  15. package/dist/admin/apps/api-keys.d.ts +84 -4
  16. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.js +54 -0
  18. package/dist/admin/apps/api-keys.js.map +1 -1
  19. package/dist/admin/apps/earn-vaults.d.ts +123 -9
  20. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  21. package/dist/admin/apps/earn-vaults.js +60 -7
  22. package/dist/admin/apps/earn-vaults.js.map +1 -1
  23. package/dist/admin/apps/organizations.d.ts.map +1 -1
  24. package/dist/admin/apps/organizations.js +4 -2
  25. package/dist/admin/apps/organizations.js.map +1 -1
  26. package/dist/admin/apps/verified-tokens.d.ts +90 -90
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.js +71 -71
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +16 -16
  31. package/dist/apps/Zones.js +4 -4
  32. package/dist/apps/data/App.d.ts +1699 -749
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/App.js +58 -9
  35. package/dist/apps/data/App.js.map +1 -1
  36. package/dist/apps/data/ZoneSelection.d.ts +62 -0
  37. package/dist/apps/data/ZoneSelection.d.ts.map +1 -0
  38. package/dist/apps/data/ZoneSelection.js +44 -0
  39. package/dist/apps/data/ZoneSelection.js.map +1 -0
  40. package/dist/apps/data/routes/activities.d.ts +2417 -50
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +1896 -1293
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +471 -340
  45. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.d.ts +12 -12
  47. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  48. package/dist/apps/data/routes/balances.js +44 -17
  49. package/dist/apps/data/routes/balances.js.map +1 -1
  50. package/dist/apps/data/routes/blocks.d.ts +42 -42
  51. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  52. package/dist/apps/data/routes/blocks.js +22 -12
  53. package/dist/apps/data/routes/blocks.js.map +1 -1
  54. package/dist/apps/data/routes/coingecko.d.ts +179 -179
  55. package/dist/apps/data/routes/earn.d.ts +144 -19
  56. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  57. package/dist/apps/data/routes/earn.js +160 -28
  58. package/dist/apps/data/routes/earn.js.map +1 -1
  59. package/dist/apps/data/routes/exchanges.d.ts +719 -13
  60. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  61. package/dist/apps/data/routes/exchanges.js +16 -13
  62. package/dist/apps/data/routes/exchanges.js.map +1 -1
  63. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  64. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  65. package/dist/apps/data/routes/fee-amm.js +9 -11
  66. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  67. package/dist/apps/data/routes/indexer.d.ts +34 -34
  68. package/dist/apps/data/routes/receipts.d.ts +363 -263
  69. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  70. package/dist/apps/data/routes/receipts.js +29 -11
  71. package/dist/apps/data/routes/receipts.js.map +1 -1
  72. package/dist/apps/data/routes/rpc.js +24 -10
  73. package/dist/apps/data/routes/rpc.js.map +1 -1
  74. package/dist/apps/data/routes/tokenlist.d.ts +13 -13
  75. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  76. package/dist/apps/data/routes/tokenlist.js +16 -15
  77. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  78. package/dist/apps/data/routes/tokens.d.ts +112 -98
  79. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  80. package/dist/apps/data/routes/tokens.js +85 -35
  81. package/dist/apps/data/routes/tokens.js.map +1 -1
  82. package/dist/apps/data/routes/transactions.d.ts +7 -2
  83. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  84. package/dist/apps/data/routes/transactions.js +38 -18
  85. package/dist/apps/data/routes/transactions.js.map +1 -1
  86. package/dist/apps/data/routes/transfers.d.ts +1 -1
  87. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  88. package/dist/apps/data/routes/transfers.js +25 -21
  89. package/dist/apps/data/routes/transfers.js.map +1 -1
  90. package/dist/apps/data/routes/valuation.d.ts +1 -1
  91. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  92. package/dist/apps/data/routes/valuation.js +12 -7
  93. package/dist/apps/data/routes/valuation.js.map +1 -1
  94. package/dist/apps/data/routes/verified-tokens.d.ts +3 -3
  95. package/dist/apps/data/routes/webhooks.d.ts +15 -15
  96. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  97. package/dist/apps/data/routes/webhooks.js +2 -2
  98. package/dist/apps/data/routes/webhooks.js.map +1 -1
  99. package/dist/apps/data/routes/zones.d.ts +32 -32
  100. package/dist/apps/data/routes/zones.js +1 -1
  101. package/dist/apps/funding/App.d.ts +38 -27
  102. package/dist/apps/funding/App.d.ts.map +1 -1
  103. package/dist/apps/funding/routes/chains.d.ts.map +1 -1
  104. package/dist/apps/funding/routes/chains.js +5 -4
  105. package/dist/apps/funding/routes/chains.js.map +1 -1
  106. package/dist/apps/funding/routes/deposit-addresses.d.ts +16 -16
  107. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  108. package/dist/apps/funding/routes/deposit-addresses.js +24 -19
  109. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  110. package/dist/apps/funding/routes/providers.d.ts +13 -13
  111. package/dist/apps/funding/routes/providers.d.ts.map +1 -1
  112. package/dist/apps/funding/routes/providers.js +3 -2
  113. package/dist/apps/funding/routes/providers.js.map +1 -1
  114. package/dist/apps/funding/routes/quotes.d.ts.map +1 -1
  115. package/dist/apps/funding/routes/quotes.js +2 -2
  116. package/dist/apps/funding/routes/quotes.js.map +1 -1
  117. package/dist/apps/funding/routes/transfers.d.ts +60 -48
  118. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  119. package/dist/apps/funding/routes/transfers.js +27 -19
  120. package/dist/apps/funding/routes/transfers.js.map +1 -1
  121. package/dist/apps/management/App.d.ts +4558 -3700
  122. package/dist/apps/management/App.d.ts.map +1 -1
  123. package/dist/apps/management/Billing.d.ts.map +1 -1
  124. package/dist/apps/management/Billing.js +19 -6
  125. package/dist/apps/management/Billing.js.map +1 -1
  126. package/dist/apps/management/routes/api-keys.d.ts +786 -175
  127. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  128. package/dist/apps/management/routes/api-keys.js +312 -34
  129. package/dist/apps/management/routes/api-keys.js.map +1 -1
  130. package/dist/apps/management/routes/billing.d.ts +103 -30
  131. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  132. package/dist/apps/management/routes/billing.js +91 -32
  133. package/dist/apps/management/routes/billing.js.map +1 -1
  134. package/dist/apps/management/routes/faucet.d.ts +35 -35
  135. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  136. package/dist/apps/management/routes/faucet.js +23 -12
  137. package/dist/apps/management/routes/faucet.js.map +1 -1
  138. package/dist/apps/management/routes/invitations.d.ts +46 -13
  139. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  140. package/dist/apps/management/routes/invitations.js +79 -29
  141. package/dist/apps/management/routes/invitations.js.map +1 -1
  142. package/dist/apps/management/routes/invite-links.d.ts +120 -75
  143. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  144. package/dist/apps/management/routes/invite-links.js +100 -41
  145. package/dist/apps/management/routes/invite-links.js.map +1 -1
  146. package/dist/apps/management/routes/me.d.ts +5 -5
  147. package/dist/apps/management/routes/me.d.ts.map +1 -1
  148. package/dist/apps/management/routes/me.js +14 -10
  149. package/dist/apps/management/routes/me.js.map +1 -1
  150. package/dist/apps/management/routes/members.d.ts +99 -78
  151. package/dist/apps/management/routes/members.d.ts.map +1 -1
  152. package/dist/apps/management/routes/members.js +37 -18
  153. package/dist/apps/management/routes/members.js.map +1 -1
  154. package/dist/apps/management/routes/orgs.d.ts +48 -8
  155. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  156. package/dist/apps/management/routes/orgs.js +48 -19
  157. package/dist/apps/management/routes/orgs.js.map +1 -1
  158. package/dist/apps/management/routes/projects.d.ts +108 -46
  159. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  160. package/dist/apps/management/routes/projects.js +72 -21
  161. package/dist/apps/management/routes/projects.js.map +1 -1
  162. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  163. package/dist/apps/management/routes/scopes.js +8 -5
  164. package/dist/apps/management/routes/scopes.js.map +1 -1
  165. package/dist/apps/management/routes/usage.d.ts +82 -82
  166. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  167. package/dist/apps/management/routes/usage.js +41 -31
  168. package/dist/apps/management/routes/usage.js.map +1 -1
  169. package/dist/apps/management/routes/verified-token-requests.d.ts +35 -7
  170. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -1
  171. package/dist/apps/management/routes/verified-token-requests.js +36 -17
  172. package/dist/apps/management/routes/verified-token-requests.js.map +1 -1
  173. package/dist/apps/management/routes/webhooks.d.ts +200 -183
  174. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  175. package/dist/apps/management/routes/webhooks.js +80 -35
  176. package/dist/apps/management/routes/webhooks.js.map +1 -1
  177. package/dist/apps/mpp/internal/Adapter.d.ts +1 -1
  178. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  179. package/dist/apps/mpp/internal/Adapter.js +12 -0
  180. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  181. package/dist/db/Db.d.ts +2 -0
  182. package/dist/db/Db.d.ts.map +1 -1
  183. package/dist/db/Db.js +52 -1
  184. package/dist/db/Db.js.map +1 -1
  185. package/dist/db/Schema.d.ts +26 -3
  186. package/dist/db/Schema.d.ts.map +1 -1
  187. package/dist/db/Schema.js +24 -5
  188. package/dist/db/Schema.js.map +1 -1
  189. package/dist/db/tables/earnVaults.d.ts +5 -1
  190. package/dist/db/tables/earnVaults.d.ts.map +1 -1
  191. package/dist/db/tables/earnVaults.js +5 -0
  192. package/dist/db/tables/earnVaults.js.map +1 -1
  193. package/dist/db/tables/fundingCatalog.d.ts +4 -2
  194. package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
  195. package/dist/db/tables/fundingCatalog.js +5 -1
  196. package/dist/db/tables/fundingCatalog.js.map +1 -1
  197. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  198. package/dist/db/tables/webhookDeliveries.js +10 -0
  199. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  200. package/dist/db/tables/webhookQueueCompletions.d.ts +15 -0
  201. package/dist/db/tables/webhookQueueCompletions.d.ts.map +1 -0
  202. package/dist/db/tables/webhookQueueCompletions.js +32 -0
  203. package/dist/db/tables/webhookQueueCompletions.js.map +1 -0
  204. package/dist/db/tables/webhookQueueEvents.d.ts +14 -20
  205. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
  206. package/dist/db/tables/webhookQueueEvents.js +68 -47
  207. package/dist/db/tables/webhookQueueEvents.js.map +1 -1
  208. package/dist/db/tables/webhookSubscriptions.d.ts +16 -5
  209. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  210. package/dist/db/tables/webhookSubscriptions.js +29 -5
  211. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  212. package/dist/handlers/internal/utils.d.ts.map +1 -1
  213. package/dist/handlers/internal/utils.js +9 -0
  214. package/dist/handlers/internal/utils.js.map +1 -1
  215. package/dist/internal/Auth.d.ts +11 -2
  216. package/dist/internal/Auth.d.ts.map +1 -1
  217. package/dist/internal/Auth.js +19 -6
  218. package/dist/internal/Auth.js.map +1 -1
  219. package/dist/internal/Cache.d.ts.map +1 -1
  220. package/dist/internal/Cache.js +11 -7
  221. package/dist/internal/Cache.js.map +1 -1
  222. package/dist/internal/EarnVaults.d.ts +43 -4
  223. package/dist/internal/EarnVaults.d.ts.map +1 -1
  224. package/dist/internal/EarnVaults.js +167 -0
  225. package/dist/internal/EarnVaults.js.map +1 -1
  226. package/dist/internal/Fetch.d.ts +23 -0
  227. package/dist/internal/Fetch.d.ts.map +1 -0
  228. package/dist/internal/Fetch.js +26 -0
  229. package/dist/internal/Fetch.js.map +1 -0
  230. package/dist/internal/Log.d.ts +3 -1
  231. package/dist/internal/Log.d.ts.map +1 -1
  232. package/dist/internal/Log.js +16 -3
  233. package/dist/internal/Log.js.map +1 -1
  234. package/dist/internal/OpenApi.d.ts +30 -24
  235. package/dist/internal/OpenApi.d.ts.map +1 -1
  236. package/dist/internal/OpenApi.js +140 -41
  237. package/dist/internal/OpenApi.js.map +1 -1
  238. package/dist/internal/Response.d.ts +14 -9
  239. package/dist/internal/Response.d.ts.map +1 -1
  240. package/dist/internal/Response.js +16 -5
  241. package/dist/internal/Response.js.map +1 -1
  242. package/dist/internal/Schema.d.ts +11 -10
  243. package/dist/internal/Schema.d.ts.map +1 -1
  244. package/dist/internal/Schema.js +14 -9
  245. package/dist/internal/Schema.js.map +1 -1
  246. package/dist/internal/Store.d.ts +28 -6
  247. package/dist/internal/Store.d.ts.map +1 -1
  248. package/dist/internal/Store.js +121 -25
  249. package/dist/internal/Store.js.map +1 -1
  250. package/dist/internal/Tidx.d.ts +2 -0
  251. package/dist/internal/Tidx.d.ts.map +1 -1
  252. package/dist/internal/Tidx.js +23 -6
  253. package/dist/internal/Tidx.js.map +1 -1
  254. package/dist/internal/Timeout.d.ts +36 -0
  255. package/dist/internal/Timeout.d.ts.map +1 -0
  256. package/dist/internal/Timeout.js +37 -0
  257. package/dist/internal/Timeout.js.map +1 -0
  258. package/dist/internal/Timing.d.ts +9 -0
  259. package/dist/internal/Timing.d.ts.map +1 -1
  260. package/dist/internal/Timing.js +7 -0
  261. package/dist/internal/Timing.js.map +1 -1
  262. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  263. package/dist/internal/WebhookDestination.js +3 -0
  264. package/dist/internal/WebhookDestination.js.map +1 -1
  265. package/dist/internal/Webhooks.d.ts +29 -12
  266. package/dist/internal/Webhooks.d.ts.map +1 -1
  267. package/dist/internal/Webhooks.js +37 -20
  268. package/dist/internal/Webhooks.js.map +1 -1
  269. package/dist/internal/funding/Action.d.ts +9 -0
  270. package/dist/internal/funding/Action.d.ts.map +1 -1
  271. package/dist/internal/funding/Action.js +16 -8
  272. package/dist/internal/funding/Action.js.map +1 -1
  273. package/dist/internal/funding/Catalog.d.ts +9 -2
  274. package/dist/internal/funding/Catalog.d.ts.map +1 -1
  275. package/dist/internal/funding/Catalog.js +19 -8
  276. package/dist/internal/funding/Catalog.js.map +1 -1
  277. package/dist/internal/funding/Deposit.d.ts +2 -2
  278. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  279. package/dist/internal/funding/Deposit.js +9 -8
  280. package/dist/internal/funding/Deposit.js.map +1 -1
  281. package/dist/internal/funding/DepositAddress.d.ts +4 -4
  282. package/dist/internal/funding/DepositAddress.d.ts.map +1 -1
  283. package/dist/internal/funding/DepositAddress.js +9 -8
  284. package/dist/internal/funding/DepositAddress.js.map +1 -1
  285. package/dist/internal/funding/Provider.d.ts +68 -13
  286. package/dist/internal/funding/Provider.d.ts.map +1 -1
  287. package/dist/internal/funding/Provider.js +42 -20
  288. package/dist/internal/funding/Provider.js.map +1 -1
  289. package/dist/internal/funding/Transfer.d.ts +13 -12
  290. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  291. package/dist/internal/funding/Transfer.js +23 -21
  292. package/dist/internal/funding/Transfer.js.map +1 -1
  293. package/dist/internal/funding/providers/stargate.d.ts +38 -0
  294. package/dist/internal/funding/providers/stargate.d.ts.map +1 -0
  295. package/dist/internal/funding/providers/stargate.js +277 -0
  296. package/dist/internal/funding/providers/stargate.js.map +1 -0
  297. package/dist/internal/index.d.ts +1 -0
  298. package/dist/internal/index.d.ts.map +1 -1
  299. package/dist/internal/index.js +1 -0
  300. package/dist/internal/index.js.map +1 -1
  301. package/dist/providers/index.d.ts +7 -0
  302. package/dist/providers/index.d.ts.map +1 -1
  303. package/dist/providers/index.js +3 -0
  304. package/dist/providers/index.js.map +1 -1
  305. package/package.json +3 -3
  306. package/src/App.test.ts +148 -0
  307. package/src/App.ts +45 -4
  308. package/src/Assets.test.ts +18 -0
  309. package/src/Assets.ts +22 -0
  310. package/src/Client.test-d.ts +311 -16
  311. package/src/Scope.test.ts +20 -0
  312. package/src/Scope.ts +16 -0
  313. package/src/admin/apps/api-keys.test.ts +75 -0
  314. package/src/admin/apps/api-keys.ts +64 -0
  315. package/src/admin/apps/earn-vaults.test.ts +143 -0
  316. package/src/admin/apps/earn-vaults.ts +84 -7
  317. package/src/admin/apps/organizations.ts +6 -2
  318. package/src/admin/ui/src/lib/api-keys.ts +19 -0
  319. package/src/admin/ui/src/routes/api-keys.tsx +125 -2
  320. package/src/admin/ui.generated.ts +71 -71
  321. package/src/apps/Zones.test.ts +4 -4
  322. package/src/apps/Zones.ts +4 -4
  323. package/src/apps/composition.test.ts +1 -1
  324. package/src/apps/data/App.test.ts +72 -0
  325. package/src/apps/data/App.ts +58 -10
  326. package/src/apps/data/ZoneSelection.test.ts +196 -0
  327. package/src/apps/data/ZoneSelection.ts +108 -0
  328. package/src/apps/data/routes/activities.test.ts +1843 -2
  329. package/src/apps/data/routes/activities.ts +2211 -1388
  330. package/src/apps/data/routes/balances.ts +134 -98
  331. package/src/apps/data/routes/blocks.test.ts +533 -0
  332. package/src/apps/data/routes/blocks.ts +36 -17
  333. package/src/apps/data/routes/earn.test.ts +187 -25
  334. package/src/apps/data/routes/earn.ts +228 -32
  335. package/src/apps/data/routes/earn.zone-access.test.ts +46 -0
  336. package/src/apps/data/routes/exchanges.test.ts +31 -1
  337. package/src/apps/data/routes/exchanges.ts +19 -15
  338. package/src/apps/data/routes/fee-amm.ts +18 -11
  339. package/src/apps/data/routes/receipts.ts +48 -17
  340. package/src/apps/data/routes/rpc.test.ts +75 -0
  341. package/src/apps/data/routes/rpc.ts +23 -9
  342. package/src/apps/data/routes/tokenlist.ts +21 -18
  343. package/src/apps/data/routes/tokens.test.ts +607 -0
  344. package/src/apps/data/routes/tokens.ts +312 -231
  345. package/src/apps/data/routes/transactions.ts +69 -30
  346. package/src/apps/data/routes/transfers.test.ts +424 -3
  347. package/src/apps/data/routes/transfers.ts +57 -36
  348. package/src/apps/data/routes/valuation.ts +94 -78
  349. package/src/apps/data/routes/webhooks.ts +6 -2
  350. package/src/apps/data/routes/zones.test.ts +21 -15
  351. package/src/apps/data/routes/zones.ts +1 -1
  352. package/src/apps/funding/routes/chains.test.ts +211 -7
  353. package/src/apps/funding/routes/chains.ts +19 -12
  354. package/src/apps/funding/routes/deposit-addresses.test.ts +416 -0
  355. package/src/apps/funding/routes/deposit-addresses.ts +101 -84
  356. package/src/apps/funding/routes/providers.ts +9 -5
  357. package/src/apps/funding/routes/quotes.test.ts +42 -0
  358. package/src/apps/funding/routes/quotes.ts +8 -5
  359. package/src/apps/funding/routes/transfers.test.ts +430 -40
  360. package/src/apps/funding/routes/transfers.ts +76 -59
  361. package/src/apps/management/Billing.test.ts +26 -0
  362. package/src/apps/management/Billing.ts +24 -7
  363. package/src/apps/management/routes/api-keys.test.ts +398 -0
  364. package/src/apps/management/routes/api-keys.ts +481 -145
  365. package/src/apps/management/routes/billing.test.ts +209 -0
  366. package/src/apps/management/routes/billing.ts +254 -177
  367. package/src/apps/management/routes/faucet.test.ts +39 -0
  368. package/src/apps/management/routes/faucet.ts +75 -41
  369. package/src/apps/management/routes/invitations.test.ts +216 -0
  370. package/src/apps/management/routes/invitations.ts +186 -109
  371. package/src/apps/management/routes/invite-links.test.ts +263 -1
  372. package/src/apps/management/routes/invite-links.ts +295 -214
  373. package/src/apps/management/routes/me.test.ts +49 -0
  374. package/src/apps/management/routes/me.ts +42 -32
  375. package/src/apps/management/routes/members.test.ts +124 -1
  376. package/src/apps/management/routes/members.ts +81 -51
  377. package/src/apps/management/routes/orgs.test.ts +176 -0
  378. package/src/apps/management/routes/orgs.ts +101 -59
  379. package/src/apps/management/routes/projects.test.ts +183 -0
  380. package/src/apps/management/routes/projects.ts +124 -60
  381. package/src/apps/management/routes/scopes.test.ts +44 -0
  382. package/src/apps/management/routes/scopes.ts +37 -28
  383. package/src/apps/management/routes/usage.test.ts +92 -0
  384. package/src/apps/management/routes/usage.ts +186 -151
  385. package/src/apps/management/routes/verified-token-requests.test.ts +114 -0
  386. package/src/apps/management/routes/verified-token-requests.ts +166 -133
  387. package/src/apps/management/routes/webhooks.test.ts +234 -0
  388. package/src/apps/management/routes/webhooks.ts +191 -78
  389. package/src/apps/mpp/internal/Adapter.ts +13 -1
  390. package/src/db/Db.test.ts +41 -0
  391. package/src/db/Db.ts +75 -1
  392. package/src/db/Schema.ts +33 -5
  393. package/src/db/tables/earnVaults.test.ts +22 -0
  394. package/src/db/tables/earnVaults.ts +11 -1
  395. package/src/db/tables/fundingCatalog.test.ts +16 -2
  396. package/src/db/tables/fundingCatalog.ts +9 -3
  397. package/src/db/tables/webhookDeliveries.ts +13 -0
  398. package/src/db/tables/webhookQueueCompletions.ts +55 -0
  399. package/src/db/tables/webhookQueueEvents.ts +95 -61
  400. package/src/db/tables/webhookSubscriptions.ts +45 -6
  401. package/src/handlers/internal/utils.test.ts +53 -0
  402. package/src/handlers/internal/utils.ts +15 -0
  403. package/src/internal/Auth.ts +35 -9
  404. package/src/internal/Cache.ts +14 -7
  405. package/src/internal/EarnVaults.capabilities.test.ts +33 -0
  406. package/src/internal/EarnVaults.test.ts +79 -0
  407. package/src/internal/EarnVaults.ts +277 -0
  408. package/src/internal/Fetch.test.ts +46 -0
  409. package/src/internal/Fetch.ts +56 -0
  410. package/src/internal/Log.test.ts +21 -0
  411. package/src/internal/Log.ts +20 -5
  412. package/src/internal/OpenApi.ts +177 -51
  413. package/src/internal/Response.test.ts +95 -0
  414. package/src/internal/Response.ts +41 -3
  415. package/src/internal/Schema.test.ts +18 -0
  416. package/src/internal/Schema.ts +19 -9
  417. package/src/internal/Store.test.ts +364 -0
  418. package/src/internal/Store.ts +153 -27
  419. package/src/internal/Tidx.test.ts +21 -1
  420. package/src/internal/Tidx.ts +23 -6
  421. package/src/internal/Timeout.test.ts +35 -0
  422. package/src/internal/Timeout.ts +73 -0
  423. package/src/internal/Timing.ts +16 -0
  424. package/src/internal/WebhookDestination.ts +3 -0
  425. package/src/internal/Webhooks.test.ts +122 -8
  426. package/src/internal/Webhooks.ts +59 -29
  427. package/src/internal/funding/Action.test.ts +41 -1
  428. package/src/internal/funding/Action.ts +69 -41
  429. package/src/internal/funding/Catalog.test.ts +34 -0
  430. package/src/internal/funding/Catalog.ts +28 -11
  431. package/src/internal/funding/Deposit.ts +71 -64
  432. package/src/internal/funding/DepositAddress.ts +50 -43
  433. package/src/internal/funding/Provider.test.ts +73 -0
  434. package/src/internal/funding/Provider.ts +246 -170
  435. package/src/internal/funding/Transfer.ts +133 -110
  436. package/src/internal/funding/providers/index.test.ts +22 -0
  437. package/src/internal/funding/providers/relay.test.ts +2 -2
  438. package/src/internal/funding/providers/stargate.test.ts +163 -0
  439. package/src/internal/funding/providers/stargate.ts +409 -0
  440. package/src/internal/index.test.ts +1 -0
  441. package/src/internal/index.ts +1 -0
  442. package/src/providers/index.test.ts +1 -0
  443. package/src/providers/index.ts +9 -0
@@ -19,12 +19,13 @@ import * as Ttl from '../../../internal/Ttl.js';
19
19
  import * as Value from '../../../internal/Value.js';
20
20
  import * as VerifiedTokens from '../../../internal/VerifiedTokens.js';
21
21
  import * as FxOracle from '../FxOracle.js';
22
+ import * as ZoneSelection from '../ZoneSelection.js';
22
23
  import * as Tokens from './tokens.js';
23
24
  import * as Transfers from './transfers.js';
24
25
  import * as Valuation from './valuation.js';
25
26
  /** Example transaction hash surfaced in OpenAPI docs. */
26
27
  const exampleHash = '0x4845ae2098724ab26a5d89370dce5124d044be6a5acc164afc348662de67d474';
27
- const tokenFields = ['token.logoUri', 'token.verified'];
28
+ const includeFields = ['token.logoUri', 'token.verified', 'zones'];
28
29
  // TIP-20 tokens live at deterministic `0x20c0…` addresses. Other contracts
29
30
  // can emit the same `Transfer` signature but do not expose TIP-20 metadata.
30
31
  const tip20AddressPrefix = '0x20c0';
@@ -36,29 +37,26 @@ export var schema;
36
37
  .union([z.literal('self'), Schema.Address])
37
38
  .check(z.describe('Who signed the activity: `self`, or the access-key address used to sign it.'));
38
39
  /** A token with required RPC metadata and optional curated fields. */
39
- schema.Token = Schema.describe(Tokens.schema.TokenReference, 'A token referenced by an activity item.');
40
- /** A token amount carrying its nominal value in the requested denomination. */
41
- const ValuedAmount = z.extend(Schema.TokenAmount, {
40
+ schema.Token = OpenApi.component(Schema.describe(Tokens.schema.TokenReference, 'A token referenced by an activity item.'), 'ActivityToken');
41
+ const ValuedAmount = Schema.describe(z.extend(Schema.TokenAmount, {
42
42
  valuation: z
43
43
  .optional(z.nullable(Valuation.schema.Value))
44
44
  .check(z.describe('The amount’s nominal value in the requested `valuation.currency`. `null` when the token is ' +
45
45
  'unverified, its display currency has no rate, or rates were unavailable.')),
46
- });
47
- /** Self-contained amount for a destination token. */
48
- const DestinationAmount = ValuedAmount.check(z.describe('Amount associated with `destinationToken`.'));
49
- /** Self-contained amount for a refunded token. */
50
- const RefundAmount = ValuedAmount.check(z.describe('Amount associated with `refund`.'));
51
- /** Self-contained amount for a source token. */
52
- const SourceAmount = ValuedAmount.check(z.describe('Amount associated with `sourceToken`.'));
46
+ }), 'A token amount carrying its nominal value in the requested denomination.');
47
+ const valuedAmount = OpenApi.component(ValuedAmount, 'ActivityValuedAmount');
48
+ const DestinationAmount = valuedAmount;
49
+ const RefundAmount = valuedAmount;
50
+ const SourceAmount = valuedAmount;
53
51
  /** Page-level resources: valuation rate provenance. */
54
- schema.Meta = z
52
+ schema.Meta = OpenApi.component(z
55
53
  .object({
56
54
  valuation: z
57
55
  .optional(Valuation.schema.Pricing)
58
56
  .check(z.describe('Rate provenance for amount valuations. Present when conversion rates were ' +
59
57
  'consulted; absent when every value was identity-valued or rates were unavailable.')),
60
58
  })
61
- .check(z.describe('Page-level resources attached to this response.'));
59
+ .check(z.describe('Page-level resources attached to this response.')), 'ActivityMeta');
62
60
  /** A token transfer with subject-relative direction and signer fields. */
63
61
  schema.Transfer = z
64
62
  .object({
@@ -96,6 +94,7 @@ export var schema;
96
94
  error: '`destinationAmount` and `destinationToken` must be returned together.',
97
95
  path: ['destinationAmount'],
98
96
  }), z.describe('One token transfer involving the requested address.'));
97
+ const activityTransfer = OpenApi.component(schema.Transfer, 'ActivityTransfer');
99
98
  /**
100
99
  * One transaction log, decoded into a known Tempo event when possible. The
101
100
  * raw fields (`address`/`data`/`logIndex`/`topics`) are always present so no
@@ -123,6 +122,7 @@ export var schema;
123
122
  .check(z.describe('Raw log topics; the first topic is the event signature hash.')),
124
123
  })
125
124
  .check(z.describe('One transaction log. Decoded fields (`eventName`/`args`/`title`) are present when it matched a known Tempo ABI; otherwise only the raw fields are set.'));
125
+ const activityEvent = OpenApi.component(event, 'ActivityEvent');
126
126
  /**
127
127
  * Fields shared by every activity item. `transactionHash`/`timestamp` identify and sort
128
128
  * the entry; `data` carries the type-specific payload — shaped like the
@@ -130,11 +130,14 @@ export var schema;
130
130
  * transfer's `direction`/`memo`/`attribution`/`signer`) the resource lacks.
131
131
  */
132
132
  const base = {
133
+ chainId: z
134
+ .optional(Schema.ChainId)
135
+ .check(z.describe('Chain ID containing this activity. Returned when `include=zones`.'), z.meta({ examples: [421700001] })),
133
136
  perspective: z
134
137
  .enum(['incoming', 'outgoing'])
135
138
  .check(z.describe('Your role in the transaction: `outgoing` when you sent or paid for it, `incoming` when it happened to you (someone else initiated it). For a `transfer`, `data.direction` is the finer-grained token-flow view.')),
136
139
  events: z
137
- .optional(z.array(event))
140
+ .optional(z.array(activityEvent))
138
141
  .check(z.describe('Every log in the transaction, decoded when possible, so nothing is hidden. Returned only when the request sets `logs=true`, except on `unknown` items where it is always present.')),
139
142
  id: z.string().check(z.describe('Stable activity ID built from the transaction hash and log index (`${transactionHash}-${logIndex}`).'), z.meta({
140
143
  examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665-3'],
@@ -150,8 +153,7 @@ export var schema;
150
153
  .check(z.describe('Human-readable label for the activity `type`, suitable for display in a feed. Past-tense, sentence case (e.g. `Token transferred`, `Tokens swapped`, `Burn blocked`).'), z.meta({ examples: ['Token transferred'] })),
151
154
  transactionHash: Schema.Hash.check(z.describe('Transaction hash for this activity.')),
152
155
  };
153
- /** The classified activity item variants (discriminated by `type`). */
154
- schema.items = [
156
+ const itemSchemas = [
155
157
  z
156
158
  .object({
157
159
  ...base,
@@ -295,7 +297,7 @@ export var schema;
295
297
  z
296
298
  .object({
297
299
  ...base,
298
- data: schema.Transfer.check(z.describe('Transfer details with feed-relative direction and signer fields.')),
300
+ data: activityTransfer.check(z.describe('Transfer details with feed-relative direction and signer fields.')),
299
301
  type: z
300
302
  .literal('transfer')
301
303
  .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
@@ -820,9 +822,7 @@ export var schema;
820
822
  data: z
821
823
  .object({
822
824
  logoUri: z.string().check(z.describe('New logo URL for the token.'), z.meta({
823
- examples: [
824
- 'https://api.tempo.xyz/data/4217/icons/0x20c0000000000000000000008f5425160ebe5525.svg',
825
- ],
825
+ examples: [Tokens.tokenExample.logoUri],
826
826
  })),
827
827
  signer: Signer,
828
828
  updater: Schema.Address.check(z.describe('Address that changed the logo.')),
@@ -1254,13 +1254,24 @@ export var schema;
1254
1254
  })
1255
1255
  .check(z.meta({ title: 'Unknown' })),
1256
1256
  ];
1257
+ /** The classified activity item variants (discriminated by `type`). */
1258
+ schema.items = itemSchemas.map((item) => {
1259
+ const [type] = item.shape.type.def.values;
1260
+ if (typeof type !== 'string')
1261
+ throw new Error('Activity item schemas require a string type.');
1262
+ const name = `ActivityItem${type
1263
+ .replace(/[^A-Za-z0-9]+(.)/g, (_, character) => character.toUpperCase())
1264
+ .replace(/^(.)/, (character) => character.toUpperCase())}`;
1265
+ return OpenApi.component(item, name);
1266
+ });
1257
1267
  /** A single classified activity item. */
1258
- schema.Item = z
1259
- .union(schema.items)
1260
- .check(z.describe('One classified activity item in the address feed.'));
1268
+ schema.Item = OpenApi.component(z.union(schema.items).check(z.describe('One classified activity item in the address feed.')), 'ActivityItem');
1261
1269
  /** A folded access-key group: consecutive same-signer items on one UTC day. */
1262
1270
  schema.Group = z
1263
1271
  .object({
1272
+ chainId: z
1273
+ .optional(Schema.ChainId)
1274
+ .check(z.describe('Chain ID containing this activity group. Returned when `include=zones`.'), z.meta({ examples: [421700001] })),
1264
1275
  data: z
1265
1276
  .object({
1266
1277
  items: z.array(schema.Item).check(z.describe('Activity items included in this group.')),
@@ -1284,16 +1295,17 @@ export var schema;
1284
1295
  type: z.literal('group'),
1285
1296
  })
1286
1297
  .check(z.meta({ title: 'Group' }), z.describe('A group of consecutive activity items signed by the same access key.'));
1298
+ const activityGroup = OpenApi.component(schema.Group, 'ActivityGroup');
1287
1299
  /** A single activity item, or a folded access-key group. */
1288
- schema.Entry = z
1289
- .union([...schema.items, schema.Group])
1290
- .check(z.describe('Either one activity item, or a grouped set of access-key activity items.'));
1291
- /** Optional token fields that require sources beyond RPC metadata. */
1300
+ schema.Entry = OpenApi.component(z
1301
+ .union([schema.Item, activityGroup])
1302
+ .check(z.describe('Either one activity item, or a grouped set of access-key activity items.')), 'ActivityEntry');
1303
+ /** Optional activity resources and selection modes. */
1292
1304
  schema.Include = z
1293
- .enum(tokenFields)
1294
- .check(z.describe('Additional token fields to include in activity responses.'));
1295
- /** Parses comma-separated optional token fields. */
1296
- schema.includeQuery = Schema.includeQuery(schema.Include, 'Comma-separated token fields to include, such as `token.logoUri,token.verified`.');
1305
+ .enum(includeFields)
1306
+ .check(z.describe('Additional activity resources and selection modes to include.'));
1307
+ /** Parses comma-separated optional activity resources and selection modes. */
1308
+ schema.includeQuery = Schema.includeQuery(schema.Include, 'Comma-separated activity options. `zones` combines the selected parent chain with every readable Zone.');
1297
1309
  /** Schemas for the getAddressActivities operation. */
1298
1310
  let getAddressActivities;
1299
1311
  (function (getAddressActivities) {
@@ -1328,7 +1340,7 @@ export var schema;
1328
1340
  * `group=true`) plus the root `nextCursor` pagination field shared with
1329
1341
  * other list endpoints.
1330
1342
  */
1331
- getAddressActivities.Response = z
1343
+ getAddressActivities.Response = OpenApi.component(z
1332
1344
  .object({
1333
1345
  data: z.array(schema.Entry).check(z.describe('Activity feed entries for the address.')),
1334
1346
  meta: z
@@ -1336,7 +1348,7 @@ export var schema;
1336
1348
  .check(z.describe('Page-level resources, such as valuation rate provenance.')),
1337
1349
  nextCursor: Schema.NextCursor,
1338
1350
  })
1339
- .check(z.describe('A page of human-readable activity for the address.'));
1351
+ .check(z.describe('A page of human-readable activity for the address.')), 'AddressActivityList');
1340
1352
  })(getAddressActivities = schema.getAddressActivities || (schema.getAddressActivities = {}));
1341
1353
  /** Schemas for the getTransactionActivities operation. */
1342
1354
  let getTransactionActivities;
@@ -1361,19 +1373,20 @@ export var schema;
1361
1373
  })
1362
1374
  .check(z.describe('Query parameters for the transaction activity request.'));
1363
1375
  /**
1364
- * The classified activity for one transaction: a list of `data` items
1365
- * (usually one headline item, occasionally more) describing what the
1366
- * transaction did onchain. Unpaginated a single transaction is finite
1367
- * so there is no `nextCursor`.
1376
+ * The classified activity for one transaction: a list of `data` items,
1377
+ * one per independent activity, describing what the transaction did
1378
+ * onchain. Unpaginated (a single transaction is finite), so there is no
1379
+ * `nextCursor`.
1368
1380
  */
1369
- getTransactionActivities.Response = z
1381
+ getTransactionActivities.Response = OpenApi.component(z
1370
1382
  .object({
1383
+ chainId: Schema.ChainId.check(z.describe('Chain ID containing the transaction.'), z.meta({ examples: [4217] })),
1371
1384
  data: z.array(schema.Item).check(z.describe('Activity items classified from the transaction.')),
1372
1385
  meta: z
1373
1386
  .optional(schema.Meta)
1374
1387
  .check(z.describe('Page-level resources, such as valuation rate provenance.')),
1375
1388
  })
1376
- .check(z.describe('The human-readable activity that happened on a transaction.'));
1389
+ .check(z.describe('The human-readable activity that happened on a transaction.')), 'TransactionActivityList');
1377
1390
  })(getTransactionActivities = schema.getTransactionActivities || (schema.getTransactionActivities = {}));
1378
1391
  })(schema || (schema = {}));
1379
1392
  /**
@@ -1462,6 +1475,7 @@ const titleByEventName = {
1462
1475
  BlacklistUpdated: 'Blacklist updated',
1463
1476
  Burn: 'Token burned',
1464
1477
  BurnBlocked: 'Burn blocked',
1478
+ Deposit: 'Assets deposited',
1465
1479
  Deposited: 'Assets deposited',
1466
1480
  EarnDeposit: 'Private assets deposited',
1467
1481
  EarnRedeem: 'Private shares redeemed',
@@ -1470,6 +1484,7 @@ const titleByEventName = {
1470
1484
  RedeemRequested: 'Share redemption requested',
1471
1485
  Redeemed: 'Shares redeemed',
1472
1486
  VenueSharesDeposited: 'Shares deposited',
1487
+ Withdraw: 'Assets withdrawn',
1473
1488
  WithdrewExact: 'Assets withdrawn',
1474
1489
  ChannelClosed: 'Channel closed',
1475
1490
  ChannelOpened: 'Channel opened',
@@ -1533,12 +1548,43 @@ const closeRequestedSelector = '0xf5a36fc00a96cbb9cf1f8f59299165e1d8ffffe94396d8
1533
1548
  const accessKeySpendSelector = '0xe0815e3aaadddf4dd75bde97fc060f0c38afe18e87a169be86a3f5c28247f192';
1534
1549
  /** TIP-20 / ERC-20 `Transfer` event signature for the decoded CTE. */
1535
1550
  const transferSignature = 'event Transfer(address indexed from, address indexed to, uint256 value)';
1551
+ // Standard ERC-4626 vault events, absent from the merged `viem/tempo` ABI.
1552
+ // Decoding them lets external vault deposits/withdrawals classify instead of
1553
+ // falling through to the share-token mint/burn legs.
1554
+ const erc4626Events = [
1555
+ {
1556
+ inputs: [
1557
+ { indexed: true, name: 'sender', type: 'address' },
1558
+ { indexed: true, name: 'owner', type: 'address' },
1559
+ { indexed: false, name: 'assets', type: 'uint256' },
1560
+ { indexed: false, name: 'shares', type: 'uint256' },
1561
+ ],
1562
+ name: 'Deposit',
1563
+ type: 'event',
1564
+ },
1565
+ {
1566
+ inputs: [
1567
+ { indexed: true, name: 'sender', type: 'address' },
1568
+ { indexed: true, name: 'receiver', type: 'address' },
1569
+ { indexed: true, name: 'owner', type: 'address' },
1570
+ { indexed: false, name: 'assets', type: 'uint256' },
1571
+ { indexed: false, name: 'shares', type: 'uint256' },
1572
+ ],
1573
+ name: 'Withdraw',
1574
+ type: 'event',
1575
+ },
1576
+ ];
1536
1577
  // TIP-20 payment-channel reserve precompile. Not exported by `viem/tempo`
1537
1578
  // `Addresses`, so the known address is pinned here: channel events
1538
1579
  // (`ChannelOpened`/`Settled`/`TopUp`/`ChannelClosed`) are emitted here, and
1539
1580
  // their paired TIP-20 `Transfer` legs use it as the funding counterparty, which
1540
1581
  // is how `channel-funded`/`channel-settled`/`channel-closed` recover the token.
1541
1582
  const tip20ChannelReserve = '0x4d50500000000000000000000000000000000000';
1583
+ // Tempo system addresses other than the DEX are never swap-route
1584
+ // counterparties; a newly shipped precompile joins the exclusion automatically.
1585
+ const nonRouteSystemAddresses = new Set([...Object.values(Addresses), tip20ChannelReserve]
1586
+ .filter((value) => value !== Addresses.stablecoinDex)
1587
+ .map((value) => value.toLowerCase()));
1542
1588
  /** Number of grouped entries to target before stopping the page-fetch loop. */
1543
1589
  const minGroupEntries = 5;
1544
1590
  /** Largest number of items folded into a single access-key group. */
@@ -1590,7 +1636,12 @@ export function addresses(options = {}) {
1590
1636
  description: 'Get a human-readable feed of what an address has been doing onchain, including payments sent and received, swaps, approvals, and more.',
1591
1637
  operationId: 'getAddressActivities',
1592
1638
  responses: OpenApi.responses({
1593
- errors: { 502: 'Tempo could not read activity or token data from an upstream service.' },
1639
+ errors: {
1640
+ 400: {
1641
+ codes: ['address_invalid', 'chain_id_invalid', 'chain_id_unsupported', 'query_invalid'],
1642
+ },
1643
+ 502: 'Tempo could not read activity or token data from an upstream service.',
1644
+ },
1594
1645
  success: {
1595
1646
  description: 'A page of address activity entries.',
1596
1647
  schema: schema.getAddressActivities.Response,
@@ -1599,7 +1650,7 @@ export function addresses(options = {}) {
1599
1650
  summary: 'List address activities',
1600
1651
  tags: ['Activities'],
1601
1652
  }), Cache.response({
1602
- cacheControl: Cache.policies.feed,
1653
+ cacheControl: Cache.policies.noStore,
1603
1654
  name: 'tempo-api:addresses:v1',
1604
1655
  key: (c) => Cache.urlKey(c, schema.getAddressActivities.Query),
1605
1656
  }), async (c) => {
@@ -1617,39 +1668,73 @@ export function addresses(options = {}) {
1617
1668
  });
1618
1669
  const { address } = c.req.valid('param');
1619
1670
  const query = c.req.valid('query');
1620
- // Cursor pages can carry current valuations and curated verification data.
1621
- if (query.cursor !== undefined)
1622
- Cache.setPolicy(c, Cache.policies.metadata);
1671
+ if (!query.include.includes('zones'))
1672
+ Cache.setPolicy(c, query.cursor === undefined ? Cache.policies.feed : Cache.policies.metadata);
1623
1673
  try {
1624
1674
  const chainId = query.chainId ?? c.get('chainId');
1625
- const provider = tidx({ chainId, tidx: c.get('getTidx')(chainId) });
1626
- const { items: unenrichedItems, nextCursor } = await Timing.time(c, 'address_activities', () => Store.memoize(() => list(provider, {
1627
- address: address,
1628
- cursor: query.cursor,
1629
- group: query.group,
1630
- limit: query.limit,
1631
- logs: query.logs,
1632
- store: c.get('store'),
1633
- }), {
1634
- key: `activities:v1:${chainId}:${address.toLowerCase()}:${query.group ?? false}:${query.logs ?? false}:${query.cursor ?? 'head'}:${query.limit}`,
1635
- store: c.get('store'),
1636
- ttl: Ttl.seconds(15),
1637
- }));
1638
- // Build immutable token and structured amount copies from RPC metadata.
1639
- const { items, rates } = await enrichTokens(c, {
1640
- chainId,
1641
- denomination: query['valuation.currency'],
1642
- include: query.include,
1643
- items: unenrichedItems,
1644
- oracle,
1675
+ const listed = await Timing.time(c, 'address_activities', async () => {
1676
+ if (!query.include.includes('zones')) {
1677
+ const result = await Store.memoize((signal) => list(tidx({ chainId, signal, tidx: c.get('getTidx')(chainId) }), {
1678
+ address: address,
1679
+ cursor: query.cursor,
1680
+ group: query.group,
1681
+ limit: query.limit,
1682
+ logs: query.logs,
1683
+ store: c.get('store'),
1684
+ }), {
1685
+ key: `activities:v1:${chainId}:${address.toLowerCase()}:${query.group ?? false}:${query.logs ?? false}:${query.cursor ?? 'head'}:${query.limit}`,
1686
+ store: c.get('store'),
1687
+ ttl: Ttl.seconds(15),
1688
+ });
1689
+ return { chainId, kind: 'chain', ...result };
1690
+ }
1691
+ const zoneChainIds = c.get('zoneChainIds');
1692
+ if (!zoneChainIds)
1693
+ throw new Error('Inferred Zone selection was not resolved.');
1694
+ const chainIds = [chainId, ...zoneChainIds];
1695
+ const result = await Store.memoize((signal) => listChains({
1696
+ address: address,
1697
+ cursor: query.cursor,
1698
+ group: query.group,
1699
+ limit: query.limit,
1700
+ logs: query.logs,
1701
+ providers: chainIds.map((chainId) => ({
1702
+ chainId,
1703
+ provider: tidx({
1704
+ chainId,
1705
+ signal,
1706
+ tidx: c.get('getTidx')(chainId),
1707
+ }),
1708
+ })),
1709
+ store: c.get('store'),
1710
+ }), {
1711
+ key: `activities:zones:v2:${chainIds.join(',')}:${address.toLowerCase()}:${query.group ?? false}:${query.logs ?? false}:${query.cursor ?? 'head'}:${query.limit}`,
1712
+ store: c.get('store'),
1713
+ ttl: Ttl.seconds(15),
1714
+ });
1715
+ return { kind: 'zones', ...result };
1645
1716
  });
1717
+ const { items, rates } = listed.kind === 'zones'
1718
+ ? await enrichChainTokens(c, {
1719
+ denomination: query['valuation.currency'],
1720
+ include: query.include,
1721
+ items: listed.items,
1722
+ oracle,
1723
+ })
1724
+ : await enrichTokens(c, {
1725
+ chainId: listed.chainId,
1726
+ denomination: query['valuation.currency'],
1727
+ include: query.include,
1728
+ items: listed.items,
1729
+ oracle,
1730
+ });
1646
1731
  // Fold consecutive access-key-signed items into `group` entries only
1647
1732
  // when requested; otherwise every item is returned at the root.
1648
1733
  const data = query.group ? groupByAccessKey(items) : items;
1649
1734
  return c.json(Response.validated(schema.getAddressActivities.Response, {
1650
1735
  data,
1651
1736
  ...(rates ? { meta: { valuation: Valuation.pricing(rates, oracle) } } : {}),
1652
- nextCursor,
1737
+ nextCursor: listed.nextCursor,
1653
1738
  }), 200);
1654
1739
  }
1655
1740
  catch (cause) {
@@ -1666,7 +1751,7 @@ export function addresses(options = {}) {
1666
1751
  * Runs the same `groupByTx` → `classifyGroup` pipeline the address feed uses
1667
1752
  * over a single transaction's logs, anchored on the transaction sender as the
1668
1753
  * viewer (a transaction-scoped feed has no subject account). Returns the
1669
- * classified item(s) usually one headline activity with the full per-tx
1754
+ * classified items (one per independent activity) with the full per-tx
1670
1755
  * event set available via `logs=true`.
1671
1756
  */
1672
1757
  export function transactions(options = {}) {
@@ -1682,10 +1767,22 @@ export function transactions(options = {}) {
1682
1767
  operationId: 'getTransactionActivities',
1683
1768
  responses: OpenApi.responses({
1684
1769
  errors: {
1770
+ 400: {
1771
+ codes: [
1772
+ 'chain_id_invalid',
1773
+ 'chain_id_unsupported',
1774
+ 'query_invalid',
1775
+ 'transaction_invalid',
1776
+ ],
1777
+ },
1685
1778
  404: {
1686
1779
  description: 'No mined transaction was found for that hash.',
1687
1780
  codes: ['transaction_not_found'],
1688
1781
  },
1782
+ 409: {
1783
+ description: 'More than one readable chain contains that transaction hash.',
1784
+ codes: ['transaction_ambiguous'],
1785
+ },
1689
1786
  502: 'Tempo could not read activity or token data from an upstream service.',
1690
1787
  },
1691
1788
  success: {
@@ -1716,36 +1813,110 @@ export function transactions(options = {}) {
1716
1813
  });
1717
1814
  const { transactionHash } = c.req.valid('param');
1718
1815
  const query = c.req.valid('query');
1719
- const chainId = query.chainId ?? c.get('chainId');
1720
1816
  try {
1721
- const unenrichedItems = await Timing.time(c, 'transaction_activities', () => Store.memoize(() => getTransactionActivities({
1722
- chainId,
1723
- logs: query.logs,
1724
- store: c.get('store'),
1725
- tidx: c.get('getTidx')(chainId),
1726
- transactionHash,
1727
- }), {
1728
- key: `activities:v1:${chainId}:tx:${transactionHash.toLowerCase()}:${query.logs ?? false}`,
1729
- store: c.get('store'),
1730
- ttl: Ttl.seconds(15),
1731
- }));
1732
- if (unenrichedItems === null)
1817
+ const result = await Timing.time(c, 'transaction_activities', async () => {
1818
+ const selected = await (async () => {
1819
+ if (!query.include.includes('zones'))
1820
+ return {
1821
+ chainId: query.chainId ?? c.get('chainId'),
1822
+ kind: 'selected',
1823
+ receipt: null,
1824
+ };
1825
+ const zoneChainIds = c.get('zoneChainIds');
1826
+ if (!zoneChainIds)
1827
+ throw new Error('Inferred Zone selection was not resolved.');
1828
+ const chainIds = [query.chainId ?? c.get('chainId'), ...zoneChainIds];
1829
+ // Cache only positive resolution. Missing transactions must remain
1830
+ // visible as soon as any selected indexer catches up.
1831
+ const match = await Store.memoize(async () => {
1832
+ const probes = await Promise.all(chainIds.map(async (chainId) => {
1833
+ try {
1834
+ return {
1835
+ chainId,
1836
+ value: await getTransactionReceipt({
1837
+ chainId,
1838
+ tidx: c.get('getTidx')(chainId),
1839
+ transactionHash,
1840
+ }),
1841
+ };
1842
+ }
1843
+ catch (cause) {
1844
+ return { cause, chainId };
1845
+ }
1846
+ }));
1847
+ return ZoneSelection.uniqueMatch(probes);
1848
+ }, {
1849
+ key: `activities:zones:v2:${chainIds.join(',')}:tx:${transactionHash.toLowerCase()}`,
1850
+ shouldCache: (match) => match.kind === 'selected',
1851
+ store: c.get('store'),
1852
+ ttl: Ttl.seconds(15),
1853
+ });
1854
+ if (match.kind === 'failed') {
1855
+ c.set('providerFailures', match.failures.map(({ cause, chainId }) => ({
1856
+ ...Tidx.providerFailure(cause),
1857
+ chainId,
1858
+ })));
1859
+ throw match.failures[0].cause;
1860
+ }
1861
+ if (match.kind !== 'selected')
1862
+ return match;
1863
+ return { chainId: match.chainId, kind: 'selected', receipt: match.value };
1864
+ })();
1865
+ if (selected.kind !== 'selected')
1866
+ return selected;
1867
+ const { chainId } = selected;
1868
+ const unenrichedItems = await Store.memoize(() => selected.receipt === null
1869
+ ? getTransactionActivities_inner({
1870
+ chainId,
1871
+ logs: query.logs,
1872
+ store: c.get('store'),
1873
+ tidx: c.get('getTidx')(chainId),
1874
+ time: (name, fn) => Timing.time(c, name, fn),
1875
+ transactionHash,
1876
+ })
1877
+ : classifyTransactionActivities({
1878
+ chainId,
1879
+ logs: query.logs,
1880
+ receipt: selected.receipt,
1881
+ store: c.get('store'),
1882
+ tidx: c.get('getTidx')(chainId),
1883
+ time: (name, fn) => Timing.time(c, name, fn),
1884
+ transactionHash,
1885
+ }), {
1886
+ key: `activities:v1:${chainId}:tx:${transactionHash.toLowerCase()}:${query.logs ?? false}`,
1887
+ store: c.get('store'),
1888
+ ttl: Ttl.seconds(15),
1889
+ });
1890
+ return unenrichedItems === null
1891
+ ? { kind: 'not_found' }
1892
+ : { chainId, items: unenrichedItems, kind: 'found' };
1893
+ });
1894
+ if (result.kind === 'not_found')
1733
1895
  return Response.error(c, {
1734
1896
  code: 'transaction_not_found',
1735
1897
  message: 'Transaction not found',
1736
1898
  status: 404,
1737
1899
  });
1900
+ if (result.kind === 'ambiguous')
1901
+ return Response.error(c, {
1902
+ code: 'transaction_ambiguous',
1903
+ message: 'Transaction hash matched more than one readable chain',
1904
+ status: 409,
1905
+ });
1738
1906
  // Mined activity can carry current valuations and verification data.
1739
- Cache.setPolicy(c, Cache.policies.metadata);
1907
+ if (!query.include.includes('zones'))
1908
+ Cache.setPolicy(c, Cache.policies.metadata);
1909
+ c.set('chainId', result.chainId);
1740
1910
  // Build immutable token and structured amount copies from RPC metadata.
1741
1911
  const { items, rates } = await enrichTokens(c, {
1742
- chainId,
1912
+ chainId: result.chainId,
1743
1913
  denomination: query['valuation.currency'],
1744
1914
  include: query.include,
1745
- items: unenrichedItems,
1915
+ items: result.items,
1746
1916
  oracle,
1747
1917
  });
1748
1918
  return c.json(Response.validated(schema.getTransactionActivities.Response, {
1919
+ chainId: result.chainId,
1749
1920
  data: items,
1750
1921
  ...(rates ? { meta: { valuation: Valuation.pricing(rates, oracle) } } : {}),
1751
1922
  }), 200);
@@ -1768,7 +1939,19 @@ export function transactions(options = {}) {
1768
1939
  * Items carry stub tokens; the caller enriches them via {@link enrichTokens}.
1769
1940
  */
1770
1941
  export async function getTransactionActivities(options) {
1771
- const { chainId, logs = false, tidx: tidxClient } = options;
1942
+ return getTransactionActivities_inner({
1943
+ ...options,
1944
+ time: async (_name, fn) => fn(),
1945
+ });
1946
+ }
1947
+ async function getTransactionActivities_inner(options) {
1948
+ const receipt = await options.time('transaction_activity_receipt', () => getTransactionReceipt(options));
1949
+ if (!receipt)
1950
+ return null;
1951
+ return classifyTransactionActivities({ ...options, receipt });
1952
+ }
1953
+ async function getTransactionReceipt(options) {
1954
+ const { chainId, tidx: tidxClient } = options;
1772
1955
  const hash = options.transactionHash.toLowerCase();
1773
1956
  // The `receipts` table is indexed by `tx_hash`, so this point lookup needs no
1774
1957
  // block bound; it also yields the block number the `logs` lookup requires.
@@ -1781,15 +1964,25 @@ export async function getTransactionActivities(options) {
1781
1964
  const receipt = receiptResult.rows[0];
1782
1965
  if (!receipt)
1783
1966
  return null;
1784
- const blockNum = Number(Value.toNumber(receipt['block_num']));
1785
- const feePayer = lowerOrNull(receipt['fee_payer']);
1786
- const txSender = lowerOrNull(receipt['from']);
1967
+ return {
1968
+ blockNum: Number(Value.toNumber(receipt['block_num'])),
1969
+ feePayer: lowerOrNull(receipt['fee_payer']),
1970
+ txSender: lowerOrNull(receipt['from']),
1971
+ };
1972
+ }
1973
+ async function classifyTransactionActivities(options) {
1974
+ const { chainId, logs = false, receipt, tidx: tidxClient } = options;
1975
+ const hash = options.transactionHash.toLowerCase();
1976
+ const { blockNum, feePayer, txSender } = receipt;
1977
+ const fingerprintMap_promise = options
1978
+ .time('transaction_activity_mpp', () => Mpp.fingerprintMap({ store: options.store }))
1979
+ .catch(() => ({}));
1787
1980
  // `logs` is sorted by block position, so a bare `tx_hash =` scans to genesis;
1788
1981
  // pair it with the receipt's block number (see `embedReceipts` in receipts.ts).
1789
- const logsResult = await tidxClient.fetch({
1982
+ const logsResult = await options.time('transaction_activity_logs', () => tidxClient.fetch({
1790
1983
  chainId,
1791
1984
  query: `SELECT tx_hash, block_num, block_timestamp, tx_idx, log_idx, address, selector, topic1, topic2, topic3, data FROM logs WHERE block_num = ${blockNum} AND tx_hash = '${hash}' ORDER BY log_idx ASC`,
1792
- });
1985
+ }));
1793
1986
  const rows = logsResult.rows.map((r) => ({
1794
1987
  blockNum: Number(Value.toNumber(r['block_num'])),
1795
1988
  blockTimestamp: Number(Value.toNumber(r['block_timestamp'])),
@@ -1812,7 +2005,7 @@ export async function getTransactionActivities(options) {
1812
2005
  return [];
1813
2006
  // Best-effort MPP service directory: a fetch failure yields an empty map, so
1814
2007
  // enrichment never blocks classification.
1815
- const fingerprintMap = await Mpp.fingerprintMap({ store: options.store }).catch(() => ({}));
2008
+ const fingerprintMap = await fingerprintMap_promise;
1816
2009
  // Anchor the viewer on the transaction sender: the sender initiated every
1817
2010
  // activity in the transaction, so `perspective` is `outgoing` and `signer`
1818
2011
  // resolves to `self`. Fall back to the zero address when the sender is absent.
@@ -1823,15 +2016,15 @@ export async function getTransactionActivities(options) {
1823
2016
  /**
1824
2017
  * Lists classified activity for an address by running the shared pipeline
1825
2018
  * (`groupByTx` → `classifyGroup` → `enrichItem`) over rows from a
1826
- * {@link Provider}. Returns enriched, chain-tagged items plus the next cursor;
1827
- * callers run {@link groupByAccessKey} over the result.
2019
+ * {@link Provider}. Returns classified items plus the next cursor; callers run
2020
+ * {@link groupByAccessKey} over the result.
1828
2021
  */
1829
2022
  export async function list(provider, options) {
1830
2023
  const { address, cursor, group = false, limit = 20, logs = false } = options;
1831
2024
  const viewer = address.toLowerCase();
1832
2025
  // Best-effort MPP service directory: a fetch failure yields an empty map, so
1833
2026
  // enrichment never blocks the feed.
1834
- const fingerprintMap = await Mpp.fingerprintMap({ store: options.store }).catch(() => ({}));
2027
+ const fingerprintMap_promise = Mpp.fingerprintMap({ store: options.store }).catch(() => ({}));
1835
2028
  let currentCursor = cursor;
1836
2029
  const allItems = [];
1837
2030
  let hasMore = false;
@@ -1846,6 +2039,7 @@ export async function list(provider, options) {
1846
2039
  const txGroups = grouped.slice(0, limit + 1);
1847
2040
  hasMore = txGroups.length > limit;
1848
2041
  const page = txGroups.slice(0, limit);
2042
+ const fingerprintMap = await fingerprintMap_promise;
1849
2043
  if (page.length === 0)
1850
2044
  break;
1851
2045
  allItems.push(...page.flatMap((txGroup) => classifyGroup(txGroup, viewer, fingerprintMap, logs).map(withTitle)));
@@ -1865,6 +2059,117 @@ export async function list(provider, options) {
1865
2059
  // public token references and structured amounts via {@link enrichTokens}.
1866
2060
  return { items: allItems, nextCursor };
1867
2061
  }
2062
+ const chainCursorVersion = 2;
2063
+ /**
2064
+ * Lists address activity across multiple chains in one stable timestamp-ordered
2065
+ * feed. Its opaque cursor retains each provider's native continuation.
2066
+ */
2067
+ export async function listChains(options) {
2068
+ const { address, cursor, limit = 20, logs = false, providers, store } = options;
2069
+ const group = options.group ?? false;
2070
+ const orderedProviders = [...providers].sort((a, b) => a.chainId - b.chainId);
2071
+ const chainIds = orderedProviders.map(({ chainId }) => chainId);
2072
+ const cursors = decodeChainCursor(cursor, chainIds);
2073
+ const viewer = address.toLowerCase();
2074
+ const fingerprintMap_promise = Mpp.fingerprintMap({ store }).catch(() => ({}));
2075
+ const states = orderedProviders.map(({ chainId, provider }) => ({
2076
+ chainId,
2077
+ exhausted: false,
2078
+ groups: [],
2079
+ provider,
2080
+ }));
2081
+ const allItems = [];
2082
+ let hasMore = false;
2083
+ for (let attempt = 0; attempt < 3; attempt++) {
2084
+ await Promise.all(states.map(async (state) => {
2085
+ if (state.exhausted || state.groups.length > 0)
2086
+ return;
2087
+ const { rows } = await state.provider.fetchRows({
2088
+ address: viewer,
2089
+ cursor: cursors.get(state.chainId),
2090
+ limit,
2091
+ });
2092
+ const groups = groupByTx(rows).slice(0, limit + 1);
2093
+ state.exhausted = groups.length <= limit;
2094
+ state.groups.push(...groups);
2095
+ }));
2096
+ const candidates = states
2097
+ .flatMap((state) => state.groups.map((txGroup) => ({ state, txGroup })))
2098
+ .sort((a, b) => {
2099
+ if (a.txGroup.blockTimestamp !== b.txGroup.blockTimestamp)
2100
+ return b.txGroup.blockTimestamp - a.txGroup.blockTimestamp;
2101
+ if (a.state.chainId !== b.state.chainId)
2102
+ return a.state.chainId - b.state.chainId;
2103
+ if (a.txGroup.blockNum !== b.txGroup.blockNum)
2104
+ return b.txGroup.blockNum - a.txGroup.blockNum;
2105
+ return b.txGroup.txIdx - a.txGroup.txIdx;
2106
+ });
2107
+ const page = candidates.slice(0, limit);
2108
+ const fingerprintMap = await fingerprintMap_promise;
2109
+ if (page.length === 0)
2110
+ break;
2111
+ for (const { state, txGroup } of page) {
2112
+ state.groups.splice(state.groups.indexOf(txGroup), 1);
2113
+ cursors.set(state.chainId, state.provider.cursorForGroup(txGroup));
2114
+ allItems.push(...classifyGroup(txGroup, viewer, fingerprintMap, logs).map((item) => ({
2115
+ ...withTitle(item),
2116
+ chainId: state.chainId,
2117
+ })));
2118
+ }
2119
+ hasMore = states.some((state) => !state.exhausted || state.groups.length > 0);
2120
+ const enough = group
2121
+ ? groupByAccessKeyItems(allItems).length >= minGroupEntries
2122
+ : allItems.length >= limit;
2123
+ if (enough || !hasMore)
2124
+ break;
2125
+ }
2126
+ // A consumed sentinel proves only that the provider may have more rows.
2127
+ // Probe drained buffers so `nextCursor` remains the end-of-list signal.
2128
+ await Promise.all(states.map(async (state) => {
2129
+ if (state.exhausted || state.groups.length > 0)
2130
+ return;
2131
+ const { rows } = await state.provider.fetchRows({
2132
+ address: viewer,
2133
+ cursor: cursors.get(state.chainId),
2134
+ limit: 1,
2135
+ });
2136
+ const group = groupByTx(rows)[0];
2137
+ if (group)
2138
+ state.groups.push(group);
2139
+ else
2140
+ state.exhausted = true;
2141
+ }));
2142
+ hasMore = states.some((state) => !state.exhausted || state.groups.length > 0);
2143
+ return {
2144
+ items: allItems,
2145
+ nextCursor: hasMore ? encodeChainCursor(cursors, chainIds) : null,
2146
+ };
2147
+ }
2148
+ function decodeChainCursor(cursor, chainIds) {
2149
+ const fields = ['int'];
2150
+ for (const _chainId of chainIds)
2151
+ fields.push('int', 'id');
2152
+ const decoded = cursor ? Cursor.decode(cursor, fields) : undefined;
2153
+ const cursors = new Map();
2154
+ if (!decoded || decoded[0] !== chainCursorVersion)
2155
+ return cursors;
2156
+ for (let index = 0; index < chainIds.length; index++) {
2157
+ const chainId = chainIds[index];
2158
+ const offset = index * 2 + 1;
2159
+ if (decoded[offset] !== chainId)
2160
+ return new Map();
2161
+ const value = decoded[offset + 1];
2162
+ if (value !== 'head')
2163
+ cursors.set(chainId, value);
2164
+ }
2165
+ return cursors;
2166
+ }
2167
+ function encodeChainCursor(cursors, chainIds) {
2168
+ return Cursor.encode([
2169
+ chainCursorVersion,
2170
+ ...chainIds.flatMap((chainId) => [chainId, cursors.get(chainId) ?? 'head']),
2171
+ ]);
2172
+ }
1868
2173
  /**
1869
2174
  * Creates a TIDX-backed {@link Provider} that runs three sub-queries in
1870
2175
  * parallel and merges them:
@@ -1875,7 +2180,16 @@ export async function list(provider, options) {
1875
2180
  * (catches access-key-signed sends where `tx.from` differs from the address).
1876
2181
  */
1877
2182
  export function tidx(deps) {
1878
- const { chainId, tidx: tidxClient } = deps;
2183
+ const { chainId, signal, tidx: tidxClient } = deps;
2184
+ const fetch = (options) => {
2185
+ const requestSignal = signal && options.signal
2186
+ ? AbortSignal.any([signal, options.signal])
2187
+ : (signal ?? options.signal);
2188
+ return tidxClient.fetch({
2189
+ ...options,
2190
+ ...(requestSignal ? { signal: requestSignal } : {}),
2191
+ });
2192
+ };
1879
2193
  function parseCursor(cursor) {
1880
2194
  // Opaque `[blockNum, txIdx]` keyset cursor, the same encoding every other
1881
2195
  // list endpoint uses; a malformed/forged cursor decodes to `undefined` and
@@ -1895,7 +2209,7 @@ export function tidx(deps) {
1895
2209
  ? `AND block_num <= ${cursor.blockNum} AND (block_num < ${cursor.blockNum} OR tx_idx < ${cursor.txIdx})`
1896
2210
  : '';
1897
2211
  const queryLimit = limit + 1;
1898
- const fetchReceipts = (side) => tidxClient.fetch({
2212
+ const fetchReceipts = (side) => fetch({
1899
2213
  chainId,
1900
2214
  // Raw-table query (no signature-decoded CTE), so the inline SQL is cast
1901
2215
  // to `string`.
@@ -1939,7 +2253,7 @@ export function tidx(deps) {
1939
2253
  const txHashes = receipts.map((receipt) => `'${receipt.txHash}'`).join(', ');
1940
2254
  // `logs` is not keyed by transaction hash, so pair the bounded hash list
1941
2255
  // with the selected receipts' block range to avoid a full archive scan.
1942
- const logs = await tidxClient.fetch({
2256
+ const logs = await fetch({
1943
2257
  chainId,
1944
2258
  // Raw-table query (no signature-decoded CTE), so the inline SQL is cast
1945
2259
  // to `string`.
@@ -1992,7 +2306,7 @@ export function tidx(deps) {
1992
2306
  `;
1993
2307
  const result = await (async () => {
1994
2308
  try {
1995
- return await tidxClient.fetch({
2309
+ return await fetch({
1996
2310
  chainId,
1997
2311
  query: query('block_num'),
1998
2312
  signatures: [transferSignature],
@@ -2003,7 +2317,7 @@ export function tidx(deps) {
2003
2317
  throw error;
2004
2318
  try {
2005
2319
  // The expression blocks a mis-selected block-ordered top-N walk.
2006
- return await tidxClient.fetch({
2320
+ return await fetch({
2007
2321
  chainId,
2008
2322
  query: query('block_num + 0'),
2009
2323
  signatures: [transferSignature],
@@ -2012,7 +2326,7 @@ export function tidx(deps) {
2012
2326
  catch (fallbackError) {
2013
2327
  if (!Tidx.isDeterministicError(fallbackError))
2014
2328
  throw fallbackError;
2015
- const result = await tidxClient.fetch({
2329
+ const result = await fetch({
2016
2330
  chainId,
2017
2331
  query: query(),
2018
2332
  signatures: [transferSignature],
@@ -2070,8 +2384,10 @@ export function mergeAndSort(q1, q2, q3) {
2070
2384
  const q1Hashes = new Set(q1.map((r) => r.txHash));
2071
2385
  const filteredQ2 = q2.filter((r) => !q1Hashes.has(r.txHash));
2072
2386
  const filteredQ3 = q3.filter((r) => !q1Hashes.has(r.txHash));
2073
- const q2Hashes = new Set(filteredQ2.map((r) => r.txHash));
2074
- const dedupedQ3 = filteredQ3.filter((r) => !q2Hashes.has(r.txHash));
2387
+ // Dedup by row, not transaction: a tx that both credits and debits the
2388
+ // viewer keeps its rows from both sides, one item each.
2389
+ const q2Keys = new Set(filteredQ2.map((r) => `${r.txHash}:${r.logIdx}`));
2390
+ const dedupedQ3 = filteredQ3.filter((r) => !q2Keys.has(`${r.txHash}:${r.logIdx}`));
2075
2391
  return [...q1, ...filteredQ2, ...dedupedQ3].sort((a, b) => {
2076
2392
  if (a.blockNum !== b.blockNum)
2077
2393
  return b.blockNum - a.blockNum;
@@ -2136,34 +2452,38 @@ function classifyGroup(group, viewer, fingerprintMap, includeLogs = false) {
2136
2452
  signer,
2137
2453
  viewer,
2138
2454
  });
2139
- // Q2-only — a transfer row without a joined receipt. Classify by comparing
2140
- // `from`/`to` against the viewer (zero-address sides are mints/burns).
2141
- if (q2Rows.length > 0) {
2142
- const r = q2Rows[0];
2143
- const base = itemBase(r.logIdx);
2144
- const from = r.transferFrom;
2145
- const to = r.transferTo;
2146
- const sourceToken = token(r.transferAmount, r.transferToken);
2147
- if (Address.isEqual(from, zeroAddress))
2148
- return [{ ...base, data: { recipient: viewer, sourceToken }, type: 'mint' }];
2149
- if (Address.isEqual(to, zeroAddress))
2150
- return [{ ...base, data: { sender: from, signer, sourceToken }, type: 'burn' }];
2151
- // Q2-only rows carry no memo, so MPP service attribution is unavailable
2152
- // here; `attribution` is resolved only on the log-bearing (Q1) path.
2153
- if (Address.isEqual(from, viewer))
2154
- return [
2155
- transfer(base, blockNum, {
2455
+ // Q2-only: transfer rows without a joined receipt, one item per row so a
2456
+ // batch paying the viewer several times surfaces every payment. Classify by
2457
+ // comparing `from`/`to` against the viewer (zero-address sides mint/burn).
2458
+ if (q2Rows.length > 0)
2459
+ return [...q2Rows]
2460
+ .sort((a, b) => a.logIdx - b.logIdx)
2461
+ .map((r) => {
2462
+ const base = itemBase(r.logIdx);
2463
+ const from = r.transferFrom;
2464
+ const to = r.transferTo;
2465
+ const sourceToken = token(r.transferAmount, r.transferToken);
2466
+ if (Address.isEqual(from, zeroAddress))
2467
+ return { ...base, data: { recipient: viewer, sourceToken }, type: 'mint' };
2468
+ if (Address.isEqual(to, zeroAddress))
2469
+ return { ...base, data: { sender: from, signer, sourceToken }, type: 'burn' };
2470
+ // Q2-only rows carry no memo, so MPP service attribution is unavailable
2471
+ // here; `attribution` is resolved only on the log-bearing (Q1) path.
2472
+ if (Address.isEqual(from, viewer))
2473
+ return transfer(base, blockNum, {
2156
2474
  direction: 'out',
2157
2475
  recipient: to,
2158
2476
  sender: viewer,
2159
2477
  signer,
2160
2478
  sourceToken,
2161
- }),
2162
- ];
2163
- return [
2164
- transfer(base, blockNum, { direction: 'in', recipient: viewer, sender: from, sourceToken }),
2165
- ];
2166
- }
2479
+ });
2480
+ return transfer(base, blockNum, {
2481
+ direction: 'in',
2482
+ recipient: viewer,
2483
+ sender: from,
2484
+ sourceToken,
2485
+ });
2486
+ });
2167
2487
  // Fallback — should not happen (a group with neither q1 nor q2 rows).
2168
2488
  return [
2169
2489
  unknown(itemBase(Math.min(...group.rows.map((r) => r.logIdx))), {
@@ -2191,7 +2511,7 @@ function classifyFromLogs(rows, ctx) {
2191
2511
  })();
2192
2512
  // When the caller requested raw logs, attach the full per-transaction log set
2193
2513
  // (decoded when possible) to every classified item so no co-located log is
2194
- // hidden behind the headline. `unknown` items already carry it.
2514
+ // hidden. `unknown` items already carry it.
2195
2515
  if (!ctx.includeLogs)
2196
2516
  return items;
2197
2517
  const events = buildTxEvents(rows, decoded);
@@ -2208,1214 +2528,1453 @@ function signerForRows(rows, options) {
2208
2528
  ? accessKeySigner
2209
2529
  : options.fallback;
2210
2530
  }
2211
- /** Classifies a Q1 (log-bearing) group via `parseEventLogs`, picking one headline item. */
2531
+ /**
2532
+ * Classifies every activity in a Q1 (log-bearing) transaction. Recognizers run
2533
+ * in priority order over a pool of decoded events, each emitting one item per
2534
+ * matched anchor and claiming the exact logs backing it.
2535
+ */
2212
2536
  function classifyFromLogsCore(rows, ctx, events = decodeRows(rows)) {
2213
2537
  const { blockNum, fingerprintMap, itemBase, viewer } = ctx;
2214
2538
  const signer = signerForRows(rows, { fallback: ctx.signer, viewer });
2215
2539
  const minLogIndex = Math.min(...rows.map((r) => r.logIdx));
2216
- // Payment-channel settlement: a CloseRequested topic plus an optional refund
2217
- // transfer back to the viewer.
2218
- const hasCloseRequested = rows.some((r) => r.selector === closeRequestedSelector);
2219
- if (hasCloseRequested) {
2220
- const closeRequestedLogIndex = rows.find((r) => r.selector === closeRequestedSelector)?.logIdx ?? minLogIndex;
2221
- const refundTransfer = events.find((e) => (e.eventName === 'Transfer' || e.eventName === 'TransferWithMemo') &&
2222
- 'to' in e.args &&
2223
- Address.isEqual(e.args.to, viewer));
2224
- const refund = refundTransfer
2225
- ? token(refundTransfer.args.amount, refundTransfer.address)
2226
- : undefined;
2227
- return [
2228
- {
2229
- ...itemBase(closeRequestedLogIndex),
2230
- data: { ...(refund ? { refund } : {}), signer },
2231
- type: 'session-closed',
2232
- },
2233
- ];
2234
- }
2235
- // Dedup preference: TransferWithMemo beats Transfer; Mint/Burn beat
2236
- // zero-address Transfer.
2237
- const preferenceMap = new Map();
2540
+ // Pair folds are counted: each TransferWithMemo folds one matching plain
2541
+ // Transfer and each Mint/Burn event folds one zero-address transfer, so an
2542
+ // extra identical payment survives as its own item. Keys require the full
2543
+ // event shape; a malformed event must not fold away a classifiable leg.
2544
+ const foldCounts = new Map();
2545
+ const addFold = (key) => foldCounts.set(key, (foldCounts.get(key) ?? 0) + 1);
2238
2546
  for (const event of events) {
2239
2547
  if (event.eventName === 'TransferWithMemo' && 'from' in event.args && 'to' in event.args) {
2240
2548
  const args = event.args;
2241
- preferenceMap.set(`${args.from}:${args.to}:${args.amount}`, 'TransferWithMemo');
2549
+ addFold(`${event.address}:${args.from}:${args.to}:${args.amount}`);
2242
2550
  }
2243
- if (event.eventName === 'Mint' && 'amount' in event.args)
2244
- preferenceMap.set(`mint:${event.address}:${event.args.amount}`, 'Mint');
2245
- if (event.eventName === 'Burn' && 'amount' in event.args)
2246
- preferenceMap.set(`burn:${event.address}:${event.args.amount}`, 'Burn');
2551
+ if (event.eventName === 'Mint' && 'amount' in event.args && 'to' in event.args)
2552
+ addFold(`mint:${event.address}:${event.args.amount}`);
2553
+ if (event.eventName === 'Burn' && 'amount' in event.args && 'from' in event.args)
2554
+ addFold(`burn:${event.address}:${event.args.amount}`);
2247
2555
  }
2248
- const dedupedEvents = events.filter((event) => {
2249
- if (event.eventName !== 'Transfer')
2556
+ const takeFold = (key) => {
2557
+ const count = foldCounts.get(key) ?? 0;
2558
+ if (count === 0)
2559
+ return false;
2560
+ foldCounts.set(key, count - 1);
2561
+ return true;
2562
+ };
2563
+ const finalEvents = events.filter((event) => {
2564
+ if (event.eventName !== 'Transfer' && event.eventName !== 'TransferWithMemo')
2565
+ return true;
2566
+ if (!('from' in event.args) || !('to' in event.args))
2250
2567
  return true;
2251
2568
  const args = event.args;
2252
- if (preferenceMap.has(`${args.from}:${args.to}:${args.amount}`))
2569
+ if (event.eventName === 'Transfer' &&
2570
+ takeFold(`${event.address}:${args.from}:${args.to}:${args.amount}`))
2253
2571
  return false;
2254
2572
  if (Address.isEqual(args.from, zeroAddress) &&
2255
- preferenceMap.has(`mint:${event.address}:${args.amount}`))
2573
+ takeFold(`mint:${event.address}:${args.amount}`))
2256
2574
  return false;
2257
2575
  if (Address.isEqual(args.to, zeroAddress) &&
2258
- preferenceMap.has(`burn:${event.address}:${args.amount}`))
2576
+ takeFold(`burn:${event.address}:${args.amount}`))
2259
2577
  return false;
2260
2578
  return true;
2261
2579
  });
2262
- const finalEvents = dedupedEvents.filter((event) => {
2263
- if (event.eventName !== 'TransferWithMemo')
2580
+ const pool = [...finalEvents].sort((a, b) => a.logIndex - b.logIndex);
2581
+ const claimed = new Set();
2582
+ const items = [];
2583
+ // Claims made while building an item are journaled, so a build that throws
2584
+ // on a malformed event releases its legs and consumes only the anchor.
2585
+ let journal;
2586
+ const claim = (event) => {
2587
+ claimed.add(event.logIndex);
2588
+ journal?.push(event.logIndex);
2589
+ };
2590
+ // A predicate throwing on a malformed decode must not discard the whole
2591
+ // transaction; treat that event as a non-match instead.
2592
+ const unclaimed = (predicate) => pool.filter((event) => {
2593
+ if (claimed.has(event.logIndex))
2594
+ return false;
2595
+ if (!predicate)
2264
2596
  return true;
2265
- const args = event.args;
2266
- if (Address.isEqual(args.from, zeroAddress) &&
2267
- preferenceMap.has(`mint:${event.address}:${args.amount}`))
2597
+ try {
2598
+ return predicate(event);
2599
+ }
2600
+ catch {
2268
2601
  return false;
2269
- if (Address.isEqual(args.to, zeroAddress) &&
2270
- preferenceMap.has(`burn:${event.address}:${args.amount}`))
2602
+ }
2603
+ });
2604
+ const lc = (value) => value.toLowerCase();
2605
+ const hx = (value) => value.toLowerCase();
2606
+ // Non-strict decoding can surface Transfer-shaped events with malformed
2607
+ // args; validating (once per event) keeps predicate comparisons throw-free.
2608
+ const transferIndexes = new Set(pool
2609
+ .filter((event) => {
2610
+ if (event.eventName !== 'Transfer' && event.eventName !== 'TransferWithMemo')
2271
2611
  return false;
2272
- return true;
2612
+ const args = event.args;
2613
+ return (typeof args.amount === 'bigint' &&
2614
+ typeof args.from === 'string' &&
2615
+ Address.validate(args.from) &&
2616
+ typeof args.to === 'string' &&
2617
+ Address.validate(args.to));
2618
+ })
2619
+ .map((event) => event.logIndex));
2620
+ const isTransfer = (event) => transferIndexes.has(event.logIndex);
2621
+ const transferArgs = (event) => event.args;
2622
+ // Finds and claims the first unclaimed event matching `predicate`, so a
2623
+ // later identical anchor pairs with the next matching leg instead of
2624
+ // reusing this one.
2625
+ const takeEvent = (predicate) => {
2626
+ for (const event of pool) {
2627
+ if (claimed.has(event.logIndex))
2628
+ continue;
2629
+ const match = (() => {
2630
+ try {
2631
+ return predicate(event);
2632
+ }
2633
+ catch {
2634
+ return false;
2635
+ }
2636
+ })();
2637
+ if (!match)
2638
+ continue;
2639
+ claim(event);
2640
+ return event;
2641
+ }
2642
+ return undefined;
2643
+ };
2644
+ const takeTransfer = (predicate) => takeEvent((event) => isTransfer(event) && predicate(event));
2645
+ // Runs `build` once per unclaimed matching event, in log order; `build`
2646
+ // claims its legs and returns the item(s) to emit, or nothing for
2647
+ // fold-only recognitions. A throwing build consumes only its anchor.
2648
+ const recognize = (predicate, build) => {
2649
+ for (const anchor of unclaimed(predicate)) {
2650
+ // A build may claim a later anchor of the same pass (or a log decoded
2651
+ // into duplicate ABI copies); never process a claimed anchor again.
2652
+ if (claimed.has(anchor.logIndex))
2653
+ continue;
2654
+ claim(anchor);
2655
+ journal = [];
2656
+ try {
2657
+ const built = build(anchor);
2658
+ if (built)
2659
+ items.push(...(Array.isArray(built) ? built : [built]));
2660
+ }
2661
+ catch {
2662
+ // Indexed logs can match a known signature without satisfying that
2663
+ // event's expected shape; release the legs and skip the anchor.
2664
+ for (const index of journal ?? [])
2665
+ claimed.delete(index);
2666
+ }
2667
+ finally {
2668
+ journal = undefined;
2669
+ }
2670
+ }
2671
+ };
2672
+ const named = (name) => (event) => event.eventName === name;
2673
+ // Claims and returns a vault operation's token legs, matched by role and
2674
+ // amount: asset legs in or out of the vault, share mint/burn legs on it.
2675
+ const claimVaultLegs = (vault, amounts) => {
2676
+ const legs = [];
2677
+ for (const event of unclaimed()) {
2678
+ const isVaultMintBurn = (event.eventName === 'Mint' || event.eventName === 'Burn') &&
2679
+ Address.isEqual(event.address, vault);
2680
+ const isVaultLeg = (() => {
2681
+ if (!isTransfer(event))
2682
+ return false;
2683
+ const args = transferArgs(event);
2684
+ if (Address.isEqual(args.from, vault) || Address.isEqual(args.to, vault))
2685
+ return true;
2686
+ return (Address.isEqual(event.address, vault) &&
2687
+ (Address.isEqual(args.from, zeroAddress) ||
2688
+ Address.isEqual(args.to, zeroAddress)));
2689
+ })();
2690
+ if (!isVaultMintBurn && !isVaultLeg)
2691
+ continue;
2692
+ const amount = event.args.amount;
2693
+ if (amount === undefined || !amounts.includes(amount))
2694
+ continue;
2695
+ claim(event);
2696
+ legs.push(event);
2697
+ }
2698
+ // The allowance granted for this operation (approve+deposit multicall) is
2699
+ // internal: exact amount, spender is the vault, on a token a leg moved.
2700
+ const legTokens = new Set(legs.map((leg) => leg.address.toLowerCase()));
2701
+ for (const event of unclaimed()) {
2702
+ if (event.eventName !== 'Approval' || !('spender' in event.args))
2703
+ continue;
2704
+ if (!legTokens.has(event.address.toLowerCase()))
2705
+ continue;
2706
+ if (!Address.isEqual(event.args.spender, vault))
2707
+ continue;
2708
+ const amount = event.args.amount;
2709
+ if (amount === undefined || !amounts.includes(amount))
2710
+ continue;
2711
+ claim(event);
2712
+ legs.push(event);
2713
+ }
2714
+ return legs;
2715
+ };
2716
+ // Private Earn router operations. Each wraps a public vault operation on the
2717
+ // vault it names; fold that vault's own event and token legs, leaving vault
2718
+ // activity elsewhere in the batch unclaimed.
2719
+ recognize(named('EarnDeposit'), (event) => {
2720
+ const args = event.args;
2721
+ const vault = lc(args.earnVault);
2722
+ // Parse before claiming so a malformed event consumes nothing but itself.
2723
+ const data = {
2724
+ actionId: args.actionId,
2725
+ assets: args.vaultAssets.toString(),
2726
+ inputAmount: args.inputAmount.toString(),
2727
+ inputToken: lc(args.inputToken),
2728
+ shares: args.earnShares.toString(),
2729
+ signer,
2730
+ status: 'completed',
2731
+ vault,
2732
+ zoneDepositHash: args.zoneDepositHash,
2733
+ };
2734
+ // Fold the vault event this router call produced: the nearest preceding
2735
+ // amount match, so a separate same-vault deposit keeps its own.
2736
+ const nested = unclaimed((e) => e.eventName === 'Deposited' &&
2737
+ e.logIndex < event.logIndex &&
2738
+ Address.isEqual(e.address, vault) &&
2739
+ e.args.assets === args.vaultAssets).at(-1);
2740
+ if (nested)
2741
+ claim(nested);
2742
+ else
2743
+ takeEvent((e) => e.eventName === 'Deposited' && Address.isEqual(e.address, vault));
2744
+ claimVaultLegs(vault, [args.inputAmount, args.vaultAssets, args.earnShares]);
2745
+ // The funding leg pays the router (the emitting contract), not the vault.
2746
+ claimVaultLegs(event.address, [args.inputAmount]);
2747
+ return { ...itemBase(event.logIndex), data, type: 'private-assets-deposited' };
2273
2748
  });
2274
- const transfers = finalEvents.filter((e) => (e.eventName === 'Transfer' || e.eventName === 'TransferWithMemo') &&
2275
- 'to' in e.args &&
2276
- 'from' in e.args &&
2277
- 'amount' in e.args);
2278
- const privateDeposit = finalEvents.find((event) => event.eventName === 'EarnDeposit');
2279
- if (privateDeposit) {
2280
- const args = privateDeposit.args;
2281
- return [
2282
- {
2283
- ...itemBase(logIndexOfEvent(privateDeposit)),
2284
- data: {
2285
- actionId: args.actionId,
2286
- assets: args.vaultAssets.toString(),
2287
- inputAmount: args.inputAmount.toString(),
2288
- inputToken: args.inputToken.toLowerCase(),
2289
- shares: args.earnShares.toString(),
2290
- signer,
2291
- status: 'completed',
2292
- vault: args.earnVault.toLowerCase(),
2293
- zoneDepositHash: args.zoneDepositHash,
2294
- },
2295
- type: 'private-assets-deposited',
2296
- },
2297
- ];
2298
- }
2299
- const privateRedeem = finalEvents.find((event) => event.eventName === 'EarnRedeem');
2300
- if (privateRedeem) {
2301
- const args = privateRedeem.args;
2302
- return [
2303
- {
2304
- ...itemBase(logIndexOfEvent(privateRedeem)),
2305
- data: {
2306
- actionId: args.actionId,
2307
- assets: args.vaultAssets.toString(),
2308
- outputAmount: args.outputAmount.toString(),
2309
- outputToken: args.outputToken.toLowerCase(),
2310
- shares: args.earnShares.toString(),
2311
- signer,
2312
- status: 'completed',
2313
- vault: args.earnVault.toLowerCase(),
2314
- zoneDepositHash: args.zoneDepositHash,
2315
- },
2316
- type: 'private-shares-redeemed',
2749
+ recognize(named('EarnRedeem'), (event) => {
2750
+ const args = event.args;
2751
+ const vault = lc(args.earnVault);
2752
+ // Parse before claiming so a malformed event consumes nothing but itself.
2753
+ const data = {
2754
+ actionId: args.actionId,
2755
+ assets: args.vaultAssets.toString(),
2756
+ outputAmount: args.outputAmount.toString(),
2757
+ outputToken: lc(args.outputToken),
2758
+ shares: args.earnShares.toString(),
2759
+ signer,
2760
+ status: 'completed',
2761
+ vault,
2762
+ zoneDepositHash: args.zoneDepositHash,
2763
+ };
2764
+ // Fold the vault event this router call produced: the nearest preceding
2765
+ // amount match, so a separate same-vault redemption keeps its own.
2766
+ const nested = unclaimed((e) => (e.eventName === 'Redeemed' || e.eventName === 'WithdrewExact') &&
2767
+ e.logIndex < event.logIndex &&
2768
+ Address.isEqual(e.address, vault) &&
2769
+ e.args.assets === args.vaultAssets).at(-1);
2770
+ if (nested)
2771
+ claim(nested);
2772
+ else
2773
+ takeEvent((e) => (e.eventName === 'Redeemed' || e.eventName === 'WithdrewExact') &&
2774
+ Address.isEqual(e.address, vault));
2775
+ claimVaultLegs(vault, [args.outputAmount, args.vaultAssets, args.earnShares]);
2776
+ // The payout leg leaves through the router (the emitting contract).
2777
+ claimVaultLegs(event.address, [args.outputAmount]);
2778
+ return { ...itemBase(event.logIndex), data, type: 'private-shares-redeemed' };
2779
+ });
2780
+ // Earn vault operations, each claiming its own token legs.
2781
+ recognize(named('Deposited'), (event) => {
2782
+ const args = event.args;
2783
+ claimVaultLegs(event.address, [args.assets, args.earnShares]);
2784
+ return {
2785
+ ...itemBase(event.logIndex),
2786
+ data: {
2787
+ assets: args.assets.toString(),
2788
+ caller: lc(args.caller),
2789
+ receiver: lc(args.receiver),
2790
+ shares: args.earnShares.toString(),
2791
+ signer,
2792
+ status: 'completed',
2793
+ vault: lc(event.address),
2317
2794
  },
2318
- ];
2319
- }
2320
- const deposited = finalEvents.find((event) => event.eventName === 'Deposited');
2321
- if (deposited) {
2322
- const args = deposited.args;
2323
- return [
2324
- {
2325
- ...itemBase(logIndexOfEvent(deposited)),
2326
- data: {
2327
- assets: args.assets.toString(),
2328
- caller: args.caller.toLowerCase(),
2329
- receiver: args.receiver.toLowerCase(),
2330
- shares: args.earnShares.toString(),
2331
- signer,
2332
- status: 'completed',
2333
- vault: deposited.address.toLowerCase(),
2334
- },
2335
- type: 'assets-deposited',
2795
+ type: 'assets-deposited',
2796
+ };
2797
+ });
2798
+ recognize(named('VenueSharesDeposited'), (event) => {
2799
+ const args = event.args;
2800
+ claimVaultLegs(event.address, [
2801
+ args.earnShares,
2802
+ args.receivedEngineShares,
2803
+ args.requestedVenueShares,
2804
+ ]);
2805
+ return {
2806
+ ...itemBase(event.logIndex),
2807
+ data: {
2808
+ caller: lc(args.caller),
2809
+ receivedEngineShares: args.receivedEngineShares.toString(),
2810
+ receiver: lc(args.receiver),
2811
+ requestedVenueShares: args.requestedVenueShares.toString(),
2812
+ shares: args.earnShares.toString(),
2813
+ signer,
2814
+ status: 'completed',
2815
+ vault: lc(event.address),
2336
2816
  },
2337
- ];
2338
- }
2339
- const venueDeposit = finalEvents.find((event) => event.eventName === 'VenueSharesDeposited');
2340
- if (venueDeposit) {
2341
- const args = venueDeposit.args;
2342
- return [
2343
- {
2344
- ...itemBase(logIndexOfEvent(venueDeposit)),
2345
- data: {
2346
- caller: args.caller.toLowerCase(),
2347
- receivedEngineShares: args.receivedEngineShares.toString(),
2348
- receiver: args.receiver.toLowerCase(),
2349
- requestedVenueShares: args.requestedVenueShares.toString(),
2350
- shares: args.earnShares.toString(),
2351
- signer,
2352
- status: 'completed',
2353
- vault: venueDeposit.address.toLowerCase(),
2354
- },
2355
- type: 'shares-deposited',
2817
+ type: 'shares-deposited',
2818
+ };
2819
+ });
2820
+ recognize(named('Redeemed'), (event) => {
2821
+ const args = event.args;
2822
+ claimVaultLegs(event.address, [args.assets, args.earnShares]);
2823
+ return {
2824
+ ...itemBase(event.logIndex),
2825
+ data: {
2826
+ assets: args.assets.toString(),
2827
+ caller: lc(args.caller),
2828
+ receiver: lc(args.receiver),
2829
+ shares: args.earnShares.toString(),
2830
+ signer,
2831
+ status: 'completed',
2832
+ vault: lc(event.address),
2356
2833
  },
2357
- ];
2358
- }
2359
- const redeemed = finalEvents.find((event) => event.eventName === 'Redeemed');
2360
- if (redeemed) {
2361
- const args = redeemed.args;
2362
- return [
2363
- {
2364
- ...itemBase(logIndexOfEvent(redeemed)),
2365
- data: {
2366
- assets: args.assets.toString(),
2367
- caller: args.caller.toLowerCase(),
2368
- receiver: args.receiver.toLowerCase(),
2369
- shares: args.earnShares.toString(),
2370
- signer,
2371
- status: 'completed',
2372
- vault: redeemed.address.toLowerCase(),
2373
- },
2374
- type: 'shares-redeemed',
2834
+ type: 'shares-redeemed',
2835
+ };
2836
+ });
2837
+ recognize(named('WithdrewExact'), (event) => {
2838
+ const args = event.args;
2839
+ claimVaultLegs(event.address, [args.assets, args.earnSharesBurned]);
2840
+ return {
2841
+ ...itemBase(event.logIndex),
2842
+ data: {
2843
+ assets: args.assets.toString(),
2844
+ caller: lc(args.caller),
2845
+ receiver: lc(args.receiver),
2846
+ sharesBurned: args.earnSharesBurned.toString(),
2847
+ signer,
2848
+ status: 'completed',
2849
+ vault: lc(event.address),
2375
2850
  },
2376
- ];
2377
- }
2378
- const withdrew = finalEvents.find((event) => event.eventName === 'WithdrewExact');
2379
- if (withdrew) {
2380
- const args = withdrew.args;
2381
- return [
2382
- {
2383
- ...itemBase(logIndexOfEvent(withdrew)),
2384
- data: {
2385
- assets: args.assets.toString(),
2386
- caller: args.caller.toLowerCase(),
2387
- sharesBurned: args.earnSharesBurned.toString(),
2388
- receiver: args.receiver.toLowerCase(),
2389
- signer,
2390
- status: 'completed',
2391
- vault: withdrew.address.toLowerCase(),
2392
- },
2393
- type: 'assets-withdrawn',
2851
+ type: 'assets-withdrawn',
2852
+ };
2853
+ });
2854
+ recognize(named('RedeemRequested'), (event) => {
2855
+ const args = event.args;
2856
+ claimVaultLegs(event.address, [args.earnShares]);
2857
+ return {
2858
+ ...itemBase(event.logIndex),
2859
+ data: {
2860
+ receiver: lc(args.receiver),
2861
+ requestId: args.requestId,
2862
+ requester: lc(args.requester),
2863
+ shares: args.earnShares.toString(),
2864
+ signer,
2865
+ status: 'pending',
2866
+ vault: lc(event.address),
2394
2867
  },
2395
- ];
2396
- }
2397
- const requested = finalEvents.find((event) => event.eventName === 'RedeemRequested');
2398
- if (requested) {
2399
- const args = requested.args;
2400
- return [
2401
- {
2402
- ...itemBase(logIndexOfEvent(requested)),
2403
- data: {
2404
- receiver: args.receiver.toLowerCase(),
2405
- requestId: args.requestId,
2406
- requester: args.requester.toLowerCase(),
2407
- shares: args.earnShares.toString(),
2408
- signer,
2409
- status: 'pending',
2410
- vault: requested.address.toLowerCase(),
2411
- },
2412
- type: 'shares-redemption-requested',
2868
+ type: 'shares-redemption-requested',
2869
+ };
2870
+ });
2871
+ recognize(named('RedeemFinalized'), (event) => {
2872
+ const args = event.args;
2873
+ claimVaultLegs(event.address, [args.assets, args.earnShares]);
2874
+ return {
2875
+ ...itemBase(event.logIndex),
2876
+ data: {
2877
+ asset: lc(args.asset),
2878
+ assets: args.assets.toString(),
2879
+ receiver: lc(args.receiver),
2880
+ requestId: args.requestId,
2881
+ shares: args.earnShares.toString(),
2882
+ signer,
2883
+ status: 'completed',
2884
+ vault: lc(event.address),
2413
2885
  },
2414
- ];
2415
- }
2416
- const finalized = finalEvents.find((event) => event.eventName === 'RedeemFinalized');
2417
- if (finalized) {
2418
- const args = finalized.args;
2419
- return [
2420
- {
2421
- ...itemBase(logIndexOfEvent(finalized)),
2422
- data: {
2423
- asset: args.asset.toLowerCase(),
2424
- assets: args.assets.toString(),
2425
- receiver: args.receiver.toLowerCase(),
2426
- requestId: args.requestId,
2427
- shares: args.earnShares.toString(),
2428
- signer,
2429
- status: 'completed',
2430
- vault: finalized.address.toLowerCase(),
2431
- },
2432
- type: 'shares-redemption-finalized',
2886
+ type: 'shares-redemption-finalized',
2887
+ };
2888
+ });
2889
+ recognize(named('RedeemCancelled'), (event) => {
2890
+ const args = event.args;
2891
+ claimVaultLegs(event.address, [args.earnShares]);
2892
+ return {
2893
+ ...itemBase(event.logIndex),
2894
+ data: {
2895
+ receiver: lc(args.receiver),
2896
+ requestId: args.requestId,
2897
+ shares: args.earnShares.toString(),
2898
+ signer,
2899
+ status: 'cancelled',
2900
+ vault: lc(event.address),
2433
2901
  },
2434
- ];
2435
- }
2436
- const cancelled = finalEvents.find((event) => event.eventName === 'RedeemCancelled');
2437
- if (cancelled) {
2438
- const args = cancelled.args;
2439
- return [
2440
- {
2441
- ...itemBase(logIndexOfEvent(cancelled)),
2442
- data: {
2443
- receiver: args.receiver.toLowerCase(),
2444
- requestId: args.requestId,
2445
- shares: args.earnShares.toString(),
2446
- signer,
2447
- status: 'cancelled',
2448
- vault: cancelled.address.toLowerCase(),
2449
- },
2450
- type: 'shares-redemption-cancelled',
2902
+ type: 'shares-redemption-cancelled',
2903
+ };
2904
+ });
2905
+ // External ERC-4626 vaults. Claim the share and asset legs unconditionally
2906
+ // so nested vault plumbing never resurfaces as burns or transfers, but emit
2907
+ // only operations the viewer is a party to. Fold-only recognitions are
2908
+ // buffered and surface only when nothing else classified.
2909
+ const suppressedVaultItems = [];
2910
+ const viewerInvolved = (parties, legs) => parties.some((party) => Address.isEqual(party, viewer)) ||
2911
+ legs.some((leg) => isTransfer(leg) &&
2912
+ (Address.isEqual(transferArgs(leg).from, viewer) ||
2913
+ Address.isEqual(transferArgs(leg).to, viewer)));
2914
+ recognize(named('Withdraw'), (event) => {
2915
+ const args = event.args;
2916
+ const vault = event.address;
2917
+ // Parse before claiming so a malformed event consumes nothing but itself.
2918
+ const data = {
2919
+ assets: args.assets.toString(),
2920
+ caller: lc(args.sender),
2921
+ receiver: lc(args.receiver),
2922
+ sharesBurned: args.shares.toString(),
2923
+ signer,
2924
+ status: 'completed',
2925
+ vault: lc(vault),
2926
+ };
2927
+ const legs = claimVaultLegs(vault, [args.assets, args.shares]);
2928
+ const item = { ...itemBase(event.logIndex), data, type: 'assets-withdrawn' };
2929
+ if (viewerInvolved([args.sender, args.receiver, args.owner], legs))
2930
+ return item;
2931
+ suppressedVaultItems.push(item);
2932
+ return undefined;
2933
+ });
2934
+ recognize(named('Deposit'), (event) => {
2935
+ const args = event.args;
2936
+ const vault = event.address;
2937
+ // Parse before claiming so a malformed event consumes nothing but itself.
2938
+ const data = {
2939
+ assets: args.assets.toString(),
2940
+ caller: lc(args.sender),
2941
+ receiver: lc(args.owner),
2942
+ shares: args.shares.toString(),
2943
+ signer,
2944
+ status: 'completed',
2945
+ vault: lc(vault),
2946
+ };
2947
+ const legs = claimVaultLegs(vault, [args.assets, args.shares]);
2948
+ const item = { ...itemBase(event.logIndex), data, type: 'assets-deposited' };
2949
+ if (viewerInvolved([args.sender, args.owner], legs))
2950
+ return item;
2951
+ suppressedVaultItems.push(item);
2952
+ return undefined;
2953
+ });
2954
+ // DEX order placed: `OrderPlaced` carries the escrow token/amount and side;
2955
+ // claim the escrow transfer that funded it.
2956
+ const orderPlacedItem = (event) => {
2957
+ const args = event.args;
2958
+ const escrow = (leg) => isTransfer(leg) &&
2959
+ Address.isEqual(leg.address, args.token) &&
2960
+ Address.isEqual(transferArgs(leg).to, event.address);
2961
+ // The escrow funds the placement, so it is the nearest preceding match; a
2962
+ // same-token earlier swap input must not be taken instead.
2963
+ const escrowLeg = unclaimed((leg) => escrow(leg) && leg.logIndex < event.logIndex && transferArgs(leg).amount === args.amount).at(-1) ?? unclaimed((leg) => escrow(leg) && leg.logIndex < event.logIndex).at(-1);
2964
+ if (escrowLeg)
2965
+ claim(escrowLeg);
2966
+ return {
2967
+ ...itemBase(event.logIndex),
2968
+ data: {
2969
+ orderId: args.orderId.toString(),
2970
+ side: args.isBid ? 'bid' : 'ask',
2971
+ signer,
2972
+ sourceToken: token(args.amount, args.token),
2973
+ tick: Number(args.tick),
2451
2974
  },
2452
- ];
2453
- }
2454
- // Swap: a transfer into the DEX paired with one out of the DEX.
2455
- const toDex = transfers.find((e) => Address.isEqual(e.args.to, Addresses.stablecoinDex));
2456
- const fromDex = transfers.find((e) => Address.isEqual(e.args.from, Addresses.stablecoinDex));
2457
- if (toDex && fromDex) {
2458
- const swapper = fromDex.args.to;
2459
- const destTokenAddress = fromDex.address;
2460
- const sourceToken = token(toDex.args.amount, toDex.address);
2461
- // Cross-token transfer: the swapped-out token is forwarded from the swapper
2462
- // to a different recipient in the same transaction (the account server's
2463
- // approve+swap+transfer multicall). Fold the swap and its delivery leg into
2464
- // one `transfer` carrying both the source token spent into the DEX and the
2465
- // destination token delivered to the recipient. A swap whose output stays
2466
- // with the swapper (no onward leg) stays a plain `swap` below.
2467
- const delivery = transfers.find((e) => Address.isEqual(e.address, destTokenAddress) &&
2468
- Address.isEqual(e.args.from, swapper) &&
2469
- !Address.isEqual(e.args.to, Addresses.stablecoinDex) &&
2470
- !Address.isEqual(e.args.to, swapper));
2471
- if (delivery) {
2472
- const args = delivery.args;
2473
- const rawMemo = delivery.eventName === 'TransferWithMemo' ? args.memo : undefined;
2474
- const memo = rawMemo ? Transfers.memoToString(rawMemo) : undefined;
2475
- const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo });
2476
- return [
2477
- transfer(itemBase(logIndexOfEvent(delivery)), blockNum, {
2478
- attribution,
2479
- destinationToken: token(args.amount, delivery.address),
2480
- direction: Address.isEqual(args.to, viewer) ? 'in' : 'out',
2481
- memo,
2482
- recipient: args.to.toLowerCase(),
2483
- sender: swapper.toLowerCase(),
2484
- signer,
2485
- sourceToken,
2486
- }),
2487
- ];
2975
+ type: 'order-placed',
2976
+ };
2977
+ };
2978
+ const isFlipOrder = (event) => 'isFlipOrder' in event.args && event.args.isFlipOrder === true;
2979
+ // Non-flip order escrow legs claim before swap pairing so they cannot
2980
+ // mispair with an unrelated swap's output leg.
2981
+ recognize((event) => event.eventName === 'OrderPlaced' && !isFlipOrder(event), orderPlacedItem);
2982
+ // Swap: a transfer into a route paired with a different token coming back
2983
+ // out. The DEX matches any swapper; other routes must be viewer-bounded
2984
+ // with route-emitted logs between the legs.
2985
+ {
2986
+ // Outgoing transfers indexed by lowercase sender, for O(candidates)
2987
+ // destination lookups. Claimed entries are skipped at scan time.
2988
+ const transfersByFrom = new Map();
2989
+ for (const event of pool) {
2990
+ if (!isTransfer(event))
2991
+ continue;
2992
+ const key = transferArgs(event).from.toLowerCase();
2993
+ const list = transfersByFrom.get(key);
2994
+ if (list)
2995
+ list.push(event);
2996
+ else
2997
+ transfersByFrom.set(key, [event]);
2998
+ }
2999
+ // One forward pass: claims only shrink the pool, so a source that finds
3000
+ // no destination now never finds one later, and scanning each source once
3001
+ // keeps the cost linear in sources instead of restarting per pair.
3002
+ for (const source of unclaimed(isTransfer)) {
3003
+ if (claimed.has(source.logIndex))
3004
+ continue;
3005
+ const sourceArgs = transferArgs(source);
3006
+ const route = sourceArgs.to;
3007
+ const routeKey = route.toLowerCase();
3008
+ // Tempo system addresses (the reserve, fee manager, outbox, and every
3009
+ // other non-DEX precompile) are never swap routes; new precompiles join
3010
+ // the exclusion automatically.
3011
+ if (nonRouteSystemAddresses.has(routeKey))
3012
+ continue;
3013
+ if (Address.isEqual(route, zeroAddress))
3014
+ continue;
3015
+ if (Address.isEqual(route, viewer))
3016
+ continue;
3017
+ const isDex = Address.isEqual(route, Addresses.stablecoinDex);
3018
+ if (!isDex && !Address.isEqual(sourceArgs.from, viewer))
3019
+ continue;
3020
+ const candidates = (transfersByFrom.get(routeKey) ?? []).filter((event) => {
3021
+ if (claimed.has(event.logIndex) || event.logIndex <= source.logIndex)
3022
+ return false;
3023
+ if (Address.isEqual(event.address, source.address))
3024
+ return false;
3025
+ return !Address.isEqual(transferArgs(event).to, Addresses.feeManager);
3026
+ });
3027
+ // Prefer the output paying the swapper back (so interleaved swaps by
3028
+ // different accounts keep their own outputs); otherwise a DEX pairs
3029
+ // first-in-first-out, and a route's last leg is a direct delivery
3030
+ // (earlier different-token legs are intermediate hops).
3031
+ const destination = candidates.find((event) => Address.isEqual(transferArgs(event).to, sourceArgs.from)) ??
3032
+ (isDex ? candidates[0] : candidates.at(-1));
3033
+ if (!destination)
3034
+ continue;
3035
+ // Route evidence for non-precompile routes: the route itself emitted a
3036
+ // log between the legs (raw rows, so undecoded router events count). A
3037
+ // counterparty that merely received and later paid back is not a route.
3038
+ if (!isDex) {
3039
+ const evidence = rows.some((r) => r.logIdx > source.logIndex &&
3040
+ r.logIdx < destination.logIndex &&
3041
+ r.logAddress.toLowerCase() === routeKey);
3042
+ if (!evidence)
3043
+ continue;
3044
+ }
3045
+ journal = [];
3046
+ try {
3047
+ claim(source);
3048
+ claim(destination);
3049
+ const destinationArgs = transferArgs(destination);
3050
+ const swapper = isDex ? destinationArgs.to : sourceArgs.from;
3051
+ const destTokenAddress = destination.address;
3052
+ const sourceToken = token(sourceArgs.amount, source.address);
3053
+ // Router plumbing between the legs: route-touching or zero-address
3054
+ // token moves and router-side approvals, never viewer-party transfers.
3055
+ // DEX-precompile pairs claim nothing here, so an interleaved swap by
3056
+ // another account keeps its own legs.
3057
+ if (!isDex)
3058
+ for (const event of unclaimed()) {
3059
+ if (event.logIndex <= source.logIndex || event.logIndex >= destination.logIndex)
3060
+ continue;
3061
+ if (isTransfer(event)) {
3062
+ const args = transferArgs(event);
3063
+ if (Address.isEqual(args.from, viewer) || Address.isEqual(args.to, viewer))
3064
+ continue;
3065
+ if (Address.isEqual(args.from, route) ||
3066
+ Address.isEqual(args.to, route) ||
3067
+ Address.isEqual(args.from, zeroAddress) ||
3068
+ Address.isEqual(args.to, zeroAddress))
3069
+ claim(event);
3070
+ }
3071
+ else if (event.eventName === 'Approval' &&
3072
+ 'owner' in event.args &&
3073
+ !Address.isEqual(event.args.owner, viewer))
3074
+ claim(event);
3075
+ }
3076
+ // The allowance granted for this swap: same token, owner, and amount,
3077
+ // and causally tied to the input leg (spender is the route, or the
3078
+ // approval immediately precedes it). Unrelated approvals stay.
3079
+ unclaimed((event) => event.eventName === 'Approval' &&
3080
+ 'amount' in event.args &&
3081
+ 'owner' in event.args &&
3082
+ 'spender' in event.args &&
3083
+ Address.isEqual(event.address, source.address) &&
3084
+ Address.isEqual(event.args.owner, sourceArgs.from) &&
3085
+ event.args.amount === sourceArgs.amount &&
3086
+ event.logIndex < source.logIndex &&
3087
+ (Address.isEqual(event.args.spender, route) ||
3088
+ event.logIndex === source.logIndex - 1))
3089
+ .slice(-1)
3090
+ .forEach(claim);
3091
+ // DEX bookkeeping this swap produced: fills against resting orders and
3092
+ // the auto-flip re-placement, acted by the swapper or its route account.
3093
+ // A fill of the viewer's own resting order stays its own item.
3094
+ const isBookkeepingActor = (value) => Address.isEqual(value, swapper) || Address.isEqual(value, route);
3095
+ unclaimed((event) => (Address.isEqual(event.address, route) ||
3096
+ Address.isEqual(event.address, Addresses.stablecoinDex)) &&
3097
+ ((event.eventName === 'OrderFilled' &&
3098
+ 'taker' in event.args &&
3099
+ isBookkeepingActor(event.args.taker) &&
3100
+ !('maker' in event.args &&
3101
+ Address.isEqual(event.args.maker, viewer))) ||
3102
+ (event.eventName === 'OrderPlaced' &&
3103
+ isFlipOrder(event) &&
3104
+ 'maker' in event.args &&
3105
+ isBookkeepingActor(event.args.maker)))).forEach(claim);
3106
+ // Cross-token transfer: the swapped-out token is forwarded whole from
3107
+ // the swapper to a new recipient after the swap (the account server's
3108
+ // approve+swap+transfer multicall), or the route delivered it to the
3109
+ // recipient directly; fold both shapes into one `transfer`. A leg
3110
+ // whose recipient later forwards a different token is the input of a
3111
+ // chained swap, not a delivery.
3112
+ const forwardsOn = (candidate) => {
3113
+ const args = transferArgs(candidate);
3114
+ return (transfersByFrom.get(args.to.toLowerCase()) ?? []).some((onward) => !claimed.has(onward.logIndex) &&
3115
+ onward.logIndex > candidate.logIndex &&
3116
+ !Address.isEqual(onward.address, candidate.address));
3117
+ };
3118
+ const delivery = !isDex && !Address.isEqual(destinationArgs.to, viewer)
3119
+ ? destination
3120
+ : takeTransfer((event) => {
3121
+ const args = transferArgs(event);
3122
+ return (event.logIndex > destination.logIndex &&
3123
+ Address.isEqual(event.address, destTokenAddress) &&
3124
+ Address.isEqual(args.from, swapper) &&
3125
+ args.amount === destinationArgs.amount &&
3126
+ !Address.isEqual(args.to, route) &&
3127
+ !Address.isEqual(args.to, swapper) &&
3128
+ !forwardsOn(event));
3129
+ });
3130
+ // A malformed delivery memo degrades to a plain swap item; the claimed
3131
+ // legs must still produce an activity.
3132
+ const item = (() => {
3133
+ if (!delivery)
3134
+ return undefined;
3135
+ try {
3136
+ const args = transferArgs(delivery);
3137
+ const rawMemo = delivery.eventName === 'TransferWithMemo' ? args.memo : undefined;
3138
+ const memo = rawMemo ? Transfers.memoToString(rawMemo) : undefined;
3139
+ const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo });
3140
+ return transfer(itemBase(delivery.logIndex), blockNum, {
3141
+ attribution,
3142
+ destinationToken: token(args.amount, delivery.address),
3143
+ direction: Address.isEqual(args.to, viewer) ? 'in' : 'out',
3144
+ memo,
3145
+ recipient: lc(args.to),
3146
+ sender: lc(swapper),
3147
+ signer,
3148
+ sourceToken,
3149
+ });
3150
+ }
3151
+ catch {
3152
+ return undefined;
3153
+ }
3154
+ })();
3155
+ items.push(item ?? {
3156
+ ...itemBase(source.logIndex),
3157
+ data: {
3158
+ destinationToken: token(destinationArgs.amount, destTokenAddress),
3159
+ signer,
3160
+ sourceToken,
3161
+ },
3162
+ type: 'swap',
3163
+ });
3164
+ }
3165
+ catch {
3166
+ // A malformed leg costs only this pair; release its claims so the
3167
+ // legs fall through to the later recognizers.
3168
+ for (const index of journal ?? [])
3169
+ claimed.delete(index);
3170
+ }
3171
+ finally {
3172
+ journal = undefined;
3173
+ }
2488
3174
  }
2489
- return [
2490
- {
2491
- ...itemBase(Math.min(logIndexOfEvent(toDex), logIndexOfEvent(fromDex))),
2492
- data: {
2493
- destinationToken: token(fromDex.args.amount, destTokenAddress),
2494
- signer,
2495
- sourceToken,
2496
- },
2497
- type: 'swap',
2498
- },
2499
- ];
2500
- }
2501
- // Mint: explicit `Mint` event (not the fee manager) or a transfer from zero.
2502
- const mintEvent = finalEvents.find((e) => e.eventName === 'Mint' &&
2503
- 'amount' in e.args &&
2504
- !Address.isEqual(e.address, Addresses.feeManager));
2505
- if (mintEvent) {
2506
- const args = mintEvent.args;
2507
- return [
2508
- {
2509
- ...itemBase(logIndexOfEvent(mintEvent)),
2510
- data: {
2511
- recipient: args.to.toLowerCase(),
2512
- sourceToken: token(args.amount, mintEvent.address),
2513
- },
2514
- type: 'mint',
2515
- },
2516
- ];
2517
- }
2518
- const zeroTransferIn = transfers.find((e) => Address.isEqual(e.args.from, zeroAddress));
2519
- if (zeroTransferIn) {
2520
- const args = zeroTransferIn.args;
2521
- return [
2522
- {
2523
- ...itemBase(logIndexOfEvent(zeroTransferIn)),
2524
- data: {
2525
- recipient: args.to.toLowerCase(),
2526
- sourceToken: token(args.amount, zeroTransferIn.address),
2527
- },
2528
- type: 'mint',
2529
- },
2530
- ];
2531
3175
  }
3176
+ // Mint: explicit `Mint` events (not the fee manager) and remaining transfers
3177
+ // from the zero address (the dedup above already folded event/transfer pairs).
3178
+ const mintItem = (event, args) => ({
3179
+ ...itemBase(event.logIndex),
3180
+ data: {
3181
+ recipient: lc(args.to),
3182
+ sourceToken: token(args.amount, event.address),
3183
+ },
3184
+ type: 'mint',
3185
+ });
3186
+ recognize((event) => event.eventName === 'Mint' &&
3187
+ 'amount' in event.args &&
3188
+ 'to' in event.args &&
3189
+ !Address.isEqual(event.address, Addresses.feeManager), (event) => mintItem(event, event.args));
3190
+ recognize((event) => isTransfer(event) &&
3191
+ Address.isEqual(transferArgs(event).from, zeroAddress), (event) => mintItem(event, transferArgs(event)));
2532
3192
  // Burn blocked: a compliance burn of receive-policy-blocked funds. It also
2533
- // emits a zero-address `Transfer` that would otherwise classify as `burn`, so
2534
- // detect the distinct `BurnBlocked` event first.
2535
- const burnBlockedEvent = finalEvents.find((e) => e.eventName === 'BurnBlocked');
2536
- if (burnBlockedEvent) {
2537
- const args = burnBlockedEvent.args;
2538
- return [
2539
- {
2540
- ...itemBase(logIndexOfEvent(burnBlockedEvent)),
2541
- data: {
2542
- sender: args.from.toLowerCase(),
2543
- signer,
2544
- sourceToken: token(args.amount, burnBlockedEvent.address),
2545
- },
2546
- type: 'burn-blocked',
2547
- },
2548
- ];
2549
- }
2550
- // Burn: explicit `Burn` event or a transfer to zero.
2551
- const burnEvent = finalEvents.find((e) => e.eventName === 'Burn' && 'amount' in e.args);
2552
- if (burnEvent) {
2553
- const args = burnEvent.args;
2554
- return [
2555
- {
2556
- ...itemBase(logIndexOfEvent(burnEvent)),
2557
- data: {
2558
- sender: args.from.toLowerCase(),
2559
- signer,
2560
- sourceToken: token(args.amount, burnEvent.address),
2561
- },
2562
- type: 'burn',
2563
- },
2564
- ];
2565
- }
2566
- const zeroTransferOut = transfers.find((e) => Address.isEqual(e.args.to, zeroAddress));
2567
- if (zeroTransferOut) {
2568
- const args = zeroTransferOut.args;
2569
- return [
2570
- {
2571
- ...itemBase(logIndexOfEvent(zeroTransferOut)),
2572
- data: {
2573
- sender: args.from.toLowerCase(),
2574
- signer,
2575
- sourceToken: token(args.amount, zeroTransferOut.address),
2576
- },
2577
- type: 'burn',
3193
+ // emits a zero-address `Transfer`; claim that paired leg so it cannot
3194
+ // classify as a plain `burn`.
3195
+ recognize(named('BurnBlocked'), (event) => {
3196
+ const args = event.args;
3197
+ takeTransfer((leg) => Address.isEqual(leg.address, event.address) &&
3198
+ Address.isEqual(transferArgs(leg).to, zeroAddress) &&
3199
+ transferArgs(leg).amount === args.amount);
3200
+ return {
3201
+ ...itemBase(event.logIndex),
3202
+ data: {
3203
+ sender: lc(args.from),
3204
+ signer,
3205
+ sourceToken: token(args.amount, event.address),
2578
3206
  },
2579
- ];
2580
- }
2581
- // Approval (excluding zone-outbox spenders).
2582
- const approvalEvent = finalEvents.find((e) => e.eventName === 'Approval' &&
3207
+ type: 'burn-blocked',
3208
+ };
3209
+ });
3210
+ // Burn: explicit `Burn` events and remaining transfers to the zero address.
3211
+ const burnItem = (event, args) => ({
3212
+ ...itemBase(event.logIndex),
3213
+ data: {
3214
+ sender: lc(args.from),
3215
+ signer,
3216
+ sourceToken: token(args.amount, event.address),
3217
+ },
3218
+ type: 'burn',
3219
+ });
3220
+ recognize((event) => event.eventName === 'Burn' && 'amount' in event.args && 'from' in event.args, (event) => burnItem(event, event.args));
3221
+ recognize((event) => isTransfer(event) && Address.isEqual(transferArgs(event).to, zeroAddress), (event) => burnItem(event, transferArgs(event)));
3222
+ // Approval (excluding zone-outbox spenders): only viewer-party approvals
3223
+ // surface here; the rest stay for the nothing-else-classified fallback.
3224
+ const isErc20Approval = (event) => {
3225
+ if (event.eventName !== 'Approval')
3226
+ return false;
3227
+ const args = event.args;
3228
+ if (typeof args.spender !== 'string' || !Address.validate(args.spender))
3229
+ return false;
2583
3230
  // ERC-721 uses the same selector but indexes its token ID as topic3. In
2584
3231
  // non-strict mode viem otherwise decodes the missing ERC-20 amount as zero.
2585
- rows.some((row) => row.logIdx === e.logIndex &&
3232
+ return (rows.some((row) => row.logIdx === event.logIndex &&
2586
3233
  row.topic3 === null &&
2587
- /^0x[0-9a-fA-F]{64}$/.test(row.data ?? '')) &&
2588
- !Address.isEqual(e.args.spender, Addresses.zoneOutbox));
2589
- if (approvalEvent) {
2590
- const args = approvalEvent.args;
2591
- return [
2592
- {
2593
- ...itemBase(logIndexOfEvent(approvalEvent)),
2594
- data: {
2595
- signer,
2596
- sourceToken: token(args.amount, approvalEvent.address),
2597
- spender: args.spender.toLowerCase(),
2598
- },
2599
- type: 'approval',
3234
+ /^0x[0-9a-fA-F]{64}$/.test(row.data ?? '')) && !Address.isEqual(args.spender, Addresses.zoneOutbox));
3235
+ };
3236
+ const approvalItem = (event) => {
3237
+ const args = event.args;
3238
+ return {
3239
+ ...itemBase(event.logIndex),
3240
+ data: {
3241
+ signer,
3242
+ sourceToken: token(args.amount, event.address),
3243
+ spender: lc(args.spender),
2600
3244
  },
2601
- ];
2602
- }
2603
- // Precompile-event activities. These run before the generic transfer
2604
- // fallback because each is emitted alongside a co-located TIP-20 `Transfer`
2605
- // whose counterparty is the precompile (the DEX, the channel reserve, the fee
2606
- // manager, the token itself), which would otherwise classify as a plain
2607
- // transfer. They are grouped by (transaction + precompile counterparty), not
2608
- // by log adjacency.
2609
- // Payment channel opened: `ChannelOpened` carries the deposit token/amount
2610
- // directly, so no paired-transfer lookup is needed.
2611
- const channelOpened = finalEvents.find((e) => e.eventName === 'ChannelOpened');
2612
- if (channelOpened) {
2613
- const args = channelOpened.args;
2614
- return [
2615
- {
2616
- ...itemBase(logIndexOfEvent(channelOpened)),
2617
- data: {
2618
- channelId: args.channelId.toLowerCase(),
2619
- payee: args.payee.toLowerCase(),
2620
- payer: args.payer.toLowerCase(),
2621
- signer,
2622
- sourceToken: token(args.deposit, args.token),
2623
- },
2624
- type: 'channel-opened',
3245
+ type: 'approval',
3246
+ };
3247
+ };
3248
+ recognize((event) => isErc20Approval(event) &&
3249
+ 'owner' in event.args &&
3250
+ (Address.isEqual(event.args.owner, viewer) ||
3251
+ Address.isEqual(event.args.spender, viewer)), approvalItem);
3252
+ // Payment channels. The reserve doesn't put the token on `TopUp`/`Settled`/
3253
+ // `ChannelClosed`, so each anchor claims its own reserve leg to recover it;
3254
+ // batched channel operations pair with their own legs one at a time.
3255
+ recognize(named('ChannelOpened'), (event) => {
3256
+ const args = event.args;
3257
+ takeTransfer((leg) => Address.isEqual(leg.address, args.token) &&
3258
+ Address.isEqual(transferArgs(leg).to, tip20ChannelReserve) &&
3259
+ transferArgs(leg).amount === args.deposit);
3260
+ return {
3261
+ ...itemBase(event.logIndex),
3262
+ data: {
3263
+ channelId: hx(args.channelId),
3264
+ payee: lc(args.payee),
3265
+ payer: lc(args.payer),
3266
+ signer,
3267
+ sourceToken: token(args.deposit, args.token),
2625
3268
  },
2626
- ];
2627
- }
2628
- // The channel reserve doesn't put the token on `TopUp`/`Settled`/
2629
- // `ChannelClosed`, so recover it from the co-located funding/payout transfer.
2630
- const reserveInflow = transfers.find((e) => Address.isEqual(e.args.to, tip20ChannelReserve));
2631
- const reserveOutflowTo = (recipient) => transfers.find((e) => Address.isEqual(e.args.from, tip20ChannelReserve) &&
2632
- Address.isEqual(e.args.to, recipient));
2633
- // Payment channel topped up: `additionalDeposit` from the event, token from
2634
- // the inbound funding transfer to the reserve.
2635
- const topUp = finalEvents.find((e) => e.eventName === 'TopUp');
2636
- if (topUp) {
2637
- const args = topUp.args;
2638
- const tokenAddress = reserveInflow
2639
- ? reserveInflow.address
3269
+ type: 'channel-opened',
3270
+ };
3271
+ });
3272
+ recognize(named('TopUp'), (event) => {
3273
+ const args = event.args;
3274
+ const inflow = takeTransfer((leg) => Address.isEqual(transferArgs(leg).to, tip20ChannelReserve) &&
3275
+ transferArgs(leg).amount === args.additionalDeposit) ?? takeTransfer((leg) => Address.isEqual(transferArgs(leg).to, tip20ChannelReserve));
3276
+ // A top-up cancels any pending close request on the channel; fold the
3277
+ // cancellation event it bundles.
3278
+ unclaimed((e) => e.eventName === 'CloseRequestCancelled' &&
3279
+ 'channelId' in e.args &&
3280
+ e.args.channelId === args.channelId)
3281
+ .slice(0, 1)
3282
+ .forEach(claim);
3283
+ const tokenAddress = inflow
3284
+ ? inflow.address
2640
3285
  : zeroAddress;
2641
- return [
2642
- {
2643
- ...itemBase(logIndexOfEvent(topUp)),
2644
- data: {
2645
- channelId: args.channelId.toLowerCase(),
2646
- signer,
2647
- sourceToken: token(args.additionalDeposit, tokenAddress),
2648
- },
2649
- type: 'channel-funded',
3286
+ return {
3287
+ ...itemBase(event.logIndex),
3288
+ data: {
3289
+ channelId: hx(args.channelId),
3290
+ signer,
3291
+ sourceToken: token(args.additionalDeposit, tokenAddress),
2650
3292
  },
2651
- ];
2652
- }
2653
- // Payment channel settled: `deltaPaid` from the event, token from the payout
2654
- // transfer out of the reserve to the payee.
2655
- const settled = finalEvents.find((e) => e.eventName === 'Settled');
2656
- if (settled) {
2657
- const args = settled.args;
2658
- const payeeLeg = reserveOutflowTo(args.payee.toLowerCase());
3293
+ type: 'channel-funded',
3294
+ };
3295
+ });
3296
+ recognize(named('Settled'), (event) => {
3297
+ const args = event.args;
3298
+ const payout = (leg) => Address.isEqual(transferArgs(leg).from, tip20ChannelReserve) &&
3299
+ Address.isEqual(transferArgs(leg).to, args.payee);
3300
+ // Prefer the amount-matched payout so batched channel operations sharing
3301
+ // a payee keep their own legs.
3302
+ const payeeLeg = takeTransfer((leg) => payout(leg) && transferArgs(leg).amount === args.deltaPaid) ??
3303
+ takeTransfer(payout);
2659
3304
  const tokenAddress = payeeLeg
2660
3305
  ? payeeLeg.address
2661
3306
  : zeroAddress;
2662
- return [
2663
- {
2664
- ...itemBase(logIndexOfEvent(settled)),
2665
- data: {
2666
- channelId: args.channelId.toLowerCase(),
2667
- payee: args.payee.toLowerCase(),
2668
- payer: args.payer.toLowerCase(),
2669
- signer,
2670
- sourceToken: token(args.deltaPaid, tokenAddress),
2671
- },
2672
- type: 'channel-settled',
2673
- },
2674
- ];
2675
- }
2676
- // Payment channel closed: payout to the payee and refund to the payer arrive
2677
- // as up to two transfers out of the reserve; attach whichever are present.
2678
- const channelClosed = finalEvents.find((e) => e.eventName === 'ChannelClosed');
2679
- if (channelClosed) {
2680
- const args = channelClosed.args;
2681
- const payeeLeg = reserveOutflowTo(args.payee.toLowerCase());
2682
- const payerLeg = reserveOutflowTo(args.payer.toLowerCase());
2683
- return [
2684
- {
2685
- ...itemBase(logIndexOfEvent(channelClosed)),
2686
- data: {
2687
- channelId: args.channelId.toLowerCase(),
2688
- payee: args.payee.toLowerCase(),
2689
- payer: args.payer.toLowerCase(),
2690
- ...(payerLeg && args.refundedToPayer > 0n
2691
- ? { refund: token(args.refundedToPayer, payerLeg.address) }
2692
- : {}),
2693
- signer,
2694
- ...(payeeLeg
2695
- ? { sourceToken: token(args.settledToPayee, payeeLeg.address) }
2696
- : {}),
2697
- },
2698
- type: 'channel-closed',
3307
+ return {
3308
+ ...itemBase(event.logIndex),
3309
+ data: {
3310
+ channelId: hx(args.channelId),
3311
+ payee: lc(args.payee),
3312
+ payer: lc(args.payer),
3313
+ signer,
3314
+ sourceToken: token(args.deltaPaid, tokenAddress),
2699
3315
  },
2700
- ];
2701
- }
2702
- // Payment channel close cancelled: the payer revoked a pending close request.
2703
- // When bundled with a top-up the `TopUp` classifier above wins; on its own
2704
- // this carries no transfer, so the event itself is the signal.
2705
- const closeCancelled = finalEvents.find((e) => e.eventName === 'CloseRequestCancelled');
2706
- if (closeCancelled) {
2707
- const args = closeCancelled.args;
2708
- return [
2709
- {
2710
- ...itemBase(logIndexOfEvent(closeCancelled)),
2711
- data: {
2712
- channelId: args.channelId.toLowerCase(),
2713
- payee: args.payee.toLowerCase(),
2714
- payer: args.payer.toLowerCase(),
2715
- signer,
2716
- },
2717
- type: 'channel-close-cancelled',
3316
+ type: 'channel-settled',
3317
+ };
3318
+ });
3319
+ recognize(named('ChannelClosed'), (event) => {
3320
+ const args = event.args;
3321
+ // A zero settlement or refund has no leg; claiming one would steal it
3322
+ // from a batched sibling operation.
3323
+ const reserveLegTo = (to, amount) => {
3324
+ if (amount === 0n)
3325
+ return undefined;
3326
+ const payout = (leg) => Address.isEqual(transferArgs(leg).from, tip20ChannelReserve) &&
3327
+ Address.isEqual(transferArgs(leg).to, to);
3328
+ return (takeTransfer((leg) => payout(leg) && transferArgs(leg).amount === amount) ??
3329
+ takeTransfer(payout));
3330
+ };
3331
+ const payeeLeg = reserveLegTo(args.payee, args.settledToPayee);
3332
+ const payerLeg = reserveLegTo(args.payer, args.refundedToPayer);
3333
+ return {
3334
+ ...itemBase(event.logIndex),
3335
+ data: {
3336
+ channelId: hx(args.channelId),
3337
+ payee: lc(args.payee),
3338
+ payer: lc(args.payer),
3339
+ ...(payerLeg && args.refundedToPayer > 0n
3340
+ ? { refund: token(args.refundedToPayer, payerLeg.address) }
3341
+ : {}),
3342
+ signer,
3343
+ ...(payeeLeg
3344
+ ? { sourceToken: token(args.settledToPayee, payeeLeg.address) }
3345
+ : {}),
2718
3346
  },
2719
- ];
2720
- }
2721
- // DEX order placed: `OrderPlaced` carries the escrow token/amount and side.
2722
- // (Auto-flip re-places are inside swaps, which already returned above.)
2723
- const orderPlaced = finalEvents.find((e) => e.eventName === 'OrderPlaced');
2724
- if (orderPlaced) {
2725
- const args = orderPlaced.args;
2726
- return [
2727
- {
2728
- ...itemBase(logIndexOfEvent(orderPlaced)),
2729
- data: {
2730
- orderId: args.orderId.toString(),
2731
- side: args.isBid ? 'bid' : 'ask',
2732
- signer,
2733
- sourceToken: token(args.amount, args.token),
2734
- tick: Number(args.tick),
2735
- },
2736
- type: 'order-placed',
3347
+ type: 'channel-closed',
3348
+ };
3349
+ });
3350
+ // Payment channel close cancelled: carries no transfer, so the event itself
3351
+ // is the signal. Cancellations bundled into a top-up were claimed above.
3352
+ recognize(named('CloseRequestCancelled'), (event) => {
3353
+ const args = event.args;
3354
+ return {
3355
+ ...itemBase(event.logIndex),
3356
+ data: {
3357
+ channelId: hx(args.channelId),
3358
+ payee: lc(args.payee),
3359
+ payer: lc(args.payer),
3360
+ signer,
2737
3361
  },
2738
- ];
3362
+ type: 'channel-close-cancelled',
3363
+ };
3364
+ });
3365
+ // Payment-channel close requested: matched on the raw selector (the event
3366
+ // is not in the merged ABI). Runs after the channel recognizers claimed
3367
+ // their own reserve legs, then takes the leftover refund back to the viewer.
3368
+ for (const row of rows.filter((r) => r.selector === closeRequestedSelector)) {
3369
+ const refundLeg = takeTransfer((event) => {
3370
+ const args = transferArgs(event);
3371
+ if (!Address.isEqual(args.to, viewer))
3372
+ return false;
3373
+ return (Address.isEqual(args.from, tip20ChannelReserve) ||
3374
+ Address.isEqual(args.from, row.logAddress));
3375
+ });
3376
+ const refund = refundLeg
3377
+ ? token(transferArgs(refundLeg).amount, refundLeg.address)
3378
+ : undefined;
3379
+ items.push({
3380
+ ...itemBase(row.logIdx),
3381
+ data: { ...(refund ? { refund } : {}), signer },
3382
+ type: 'session-closed',
3383
+ });
2739
3384
  }
3385
+ // Auto-flip order placements not folded into a swap above.
3386
+ recognize(named('OrderPlaced'), orderPlacedItem);
2740
3387
  // DEX order cancelled: refund is credited to the internal DEX balance, so
2741
- // there is no co-located transfer the `OrderCancelled` event is the signal.
2742
- const orderCancelled = finalEvents.find((e) => e.eventName === 'OrderCancelled');
2743
- if (orderCancelled) {
2744
- const args = orderCancelled.args;
2745
- return [
2746
- {
2747
- ...itemBase(logIndexOfEvent(orderCancelled)),
2748
- data: { orderId: args.orderId.toString(), signer },
2749
- type: 'order-cancelled',
2750
- },
2751
- ];
2752
- }
2753
- // Fee-AMM rebalance swap: `validatorToken` in, `userToken` out.
2754
- const rebalanceSwap = finalEvents.find((e) => e.eventName === 'RebalanceSwap');
2755
- if (rebalanceSwap) {
2756
- const args = rebalanceSwap.args;
2757
- return [
2758
- {
2759
- ...itemBase(logIndexOfEvent(rebalanceSwap)),
2760
- data: {
2761
- destinationToken: token(args.amountOut, args.userToken),
2762
- signer,
2763
- sourceToken: token(args.amountIn, args.validatorToken),
2764
- swapper: args.swapper.toLowerCase(),
2765
- },
2766
- type: 'fee-rebalance-swap',
3388
+ // there is no co-located transfer; the event itself is the signal.
3389
+ recognize(named('OrderCancelled'), (event) => {
3390
+ const args = event.args;
3391
+ return {
3392
+ ...itemBase(event.logIndex),
3393
+ data: { orderId: args.orderId.toString(), signer },
3394
+ type: 'order-cancelled',
3395
+ };
3396
+ });
3397
+ // Fee-AMM rebalance swap: `validatorToken` in, `userToken` out; claim both
3398
+ // fee-manager legs so neither resurfaces as a plain transfer.
3399
+ recognize(named('RebalanceSwap'), (event) => {
3400
+ const args = event.args;
3401
+ takeTransfer((leg) => Address.isEqual(leg.address, args.validatorToken) &&
3402
+ Address.isEqual(transferArgs(leg).from, args.swapper) &&
3403
+ Address.isEqual(transferArgs(leg).to, Addresses.feeManager) &&
3404
+ transferArgs(leg).amount === args.amountIn);
3405
+ takeTransfer((leg) => Address.isEqual(leg.address, args.userToken) &&
3406
+ Address.isEqual(transferArgs(leg).from, Addresses.feeManager) &&
3407
+ Address.isEqual(transferArgs(leg).to, args.swapper) &&
3408
+ transferArgs(leg).amount === args.amountOut);
3409
+ return {
3410
+ ...itemBase(event.logIndex),
3411
+ data: {
3412
+ destinationToken: token(args.amountOut, args.userToken),
3413
+ signer,
3414
+ sourceToken: token(args.amountIn, args.validatorToken),
3415
+ swapper: lc(args.swapper),
2767
3416
  },
2768
- ];
2769
- }
2770
- // Validator fees distributed.
2771
- const feesDistributed = finalEvents.find((e) => e.eventName === 'FeesDistributed');
2772
- if (feesDistributed) {
2773
- const args = feesDistributed.args;
2774
- return [
2775
- {
2776
- ...itemBase(logIndexOfEvent(feesDistributed)),
2777
- data: {
2778
- signer,
2779
- sourceToken: token(args.amount, args.token),
2780
- validator: args.validator.toLowerCase(),
2781
- },
2782
- type: 'fees-distributed',
3417
+ type: 'fee-rebalance-swap',
3418
+ };
3419
+ });
3420
+ // Validator fees distributed; claim the fee-manager payout leg.
3421
+ recognize(named('FeesDistributed'), (event) => {
3422
+ const args = event.args;
3423
+ takeTransfer((leg) => Address.isEqual(leg.address, args.token) &&
3424
+ Address.isEqual(transferArgs(leg).from, Addresses.feeManager) &&
3425
+ Address.isEqual(transferArgs(leg).to, args.validator) &&
3426
+ transferArgs(leg).amount === args.amount);
3427
+ return {
3428
+ ...itemBase(event.logIndex),
3429
+ data: {
3430
+ signer,
3431
+ sourceToken: token(args.amount, args.token),
3432
+ validator: lc(args.validator),
2783
3433
  },
2784
- ];
2785
- }
2786
- // Reward pool funded: the funded token is the emitting token contract itself.
2787
- const rewardDistributed = finalEvents.find((e) => e.eventName === 'RewardDistributed');
2788
- if (rewardDistributed) {
2789
- const args = rewardDistributed.args;
2790
- return [
2791
- {
2792
- ...itemBase(logIndexOfEvent(rewardDistributed)),
2793
- data: {
2794
- funder: args.funder.toLowerCase(),
2795
- signer,
2796
- sourceToken: token(args.amount, rewardDistributed.address),
2797
- },
2798
- type: 'reward-distributed',
3434
+ type: 'fees-distributed',
3435
+ };
3436
+ });
3437
+ // Reward pool funded: the funded token is the emitting contract itself;
3438
+ // claim the funding transfer.
3439
+ recognize(named('RewardDistributed'), (event) => {
3440
+ const args = event.args;
3441
+ takeTransfer((leg) => Address.isEqual(leg.address, event.address) &&
3442
+ Address.isEqual(transferArgs(leg).from, args.funder) &&
3443
+ Address.isEqual(transferArgs(leg).to, event.address) &&
3444
+ transferArgs(leg).amount === args.amount);
3445
+ return {
3446
+ ...itemBase(event.logIndex),
3447
+ data: {
3448
+ funder: lc(args.funder),
3449
+ signer,
3450
+ sourceToken: token(args.amount, event.address),
2799
3451
  },
2800
- ];
2801
- }
3452
+ type: 'reward-distributed',
3453
+ };
3454
+ });
2802
3455
  // Reward recipient redirected.
2803
- const rewardRecipientSet = finalEvents.find((e) => e.eventName === 'RewardRecipientSet');
2804
- if (rewardRecipientSet) {
2805
- const args = rewardRecipientSet.args;
2806
- return [
2807
- {
2808
- ...itemBase(logIndexOfEvent(rewardRecipientSet)),
2809
- data: {
2810
- holder: args.holder.toLowerCase(),
2811
- recipient: args.recipient.toLowerCase(),
2812
- signer,
2813
- },
2814
- type: 'reward-recipient-set',
3456
+ recognize(named('RewardRecipientSet'), (event) => {
3457
+ const args = event.args;
3458
+ return {
3459
+ ...itemBase(event.logIndex),
3460
+ data: {
3461
+ holder: lc(args.holder),
3462
+ recipient: lc(args.recipient),
3463
+ signer,
2815
3464
  },
2816
- ];
2817
- }
3465
+ type: 'reward-recipient-set',
3466
+ };
3467
+ });
2818
3468
  // Access-key spending limit updated: `newLimit` is carried as the token
2819
3469
  // amount so it enriches with the token's metadata.
2820
- const spendingLimitUpdated = finalEvents.find((e) => e.eventName === 'SpendingLimitUpdated');
2821
- if (spendingLimitUpdated) {
2822
- const args = spendingLimitUpdated.args;
2823
- return [
2824
- {
2825
- ...itemBase(logIndexOfEvent(spendingLimitUpdated)),
2826
- data: {
2827
- account: args.account.toLowerCase(),
2828
- publicKey: args.publicKey.toLowerCase(),
2829
- signer,
2830
- sourceToken: token(args.newLimit, args.token),
2831
- },
2832
- type: 'spending-limit-updated',
2833
- },
2834
- ];
2835
- }
2836
- // Access-key lifecycle events — emitted alongside any co-located transfer.
2837
- const accessKeyItems = [];
2838
- for (const event of finalEvents) {
2839
- if (event.eventName === 'KeyAuthorized' &&
2840
- Address.isEqual(event.address, Addresses.accountKeychain)) {
2841
- const args = event.args;
2842
- accessKeyItems.push({
2843
- ...itemBase(logIndexOfEvent(event)),
2844
- data: {
2845
- account: args.account.toLowerCase(),
2846
- expiry: Number(args.expiry),
2847
- publicKey: args.publicKey.toLowerCase(),
2848
- signatureType: Number(args.signatureType),
2849
- },
2850
- type: 'access-key-created',
2851
- });
2852
- }
2853
- else if (event.eventName === 'KeyRevoked' &&
2854
- Address.isEqual(event.address, Addresses.accountKeychain)) {
2855
- const args = event.args;
2856
- accessKeyItems.push({
2857
- ...itemBase(logIndexOfEvent(event)),
2858
- data: {
2859
- account: args.account.toLowerCase(),
2860
- publicKey: args.publicKey.toLowerCase(),
2861
- },
2862
- type: 'access-key-revoked',
2863
- });
2864
- }
2865
- }
2866
- // Token creation.
2867
- const tokenCreated = finalEvents.find((e) => e.eventName === 'TokenCreated' && Address.isEqual(e.address, Addresses.tip20Factory));
2868
- if (tokenCreated) {
2869
- const args = tokenCreated.args;
2870
- return [
2871
- {
2872
- ...itemBase(logIndexOfEvent(tokenCreated)),
2873
- data: {
2874
- address: args.token.toLowerCase(),
2875
- currency: args.currency,
2876
- name: args.name,
2877
- symbol: args.symbol,
2878
- },
2879
- type: 'token-created',
2880
- },
2881
- ...accessKeyItems,
2882
- ];
2883
- }
2884
- // Governance / admin / DEX-bookkeeping events. Each maps a single decoded
2885
- // event to a typed item. Checked after token creation (a token-creation tx
2886
- // also emits `RoleMembershipUpdated`) and before the plain-transfer fallback;
2887
- // these carry no token movement, so the event itself is the whole signal.
2888
- const lc = (value) => value.toLowerCase();
2889
- const hx = (value) => value.toLowerCase();
2890
- const findEvent = (name) => finalEvents.find((e) => e.eventName === name);
2891
- // DEX bookkeeping (taker swaps and `OrderPlaced` already returned above).
2892
- const orderFilled = findEvent('OrderFilled');
2893
- if (orderFilled) {
2894
- const a = orderFilled.args;
2895
- return [
2896
- {
2897
- ...itemBase(logIndexOfEvent(orderFilled)),
2898
- data: {
2899
- amountFilled: a.amountFilled.toString(),
2900
- maker: lc(a.maker),
2901
- orderId: a.orderId.toString(),
2902
- partialFill: a.partialFill,
2903
- signer,
2904
- taker: lc(a.taker),
2905
- },
2906
- type: 'order-filled',
2907
- },
2908
- ];
2909
- }
2910
- const orderFlipped = findEvent('OrderFlipped');
2911
- if (orderFlipped) {
2912
- const a = orderFlipped.args;
2913
- return [
2914
- {
2915
- ...itemBase(logIndexOfEvent(orderFlipped)),
2916
- data: {
2917
- flipTick: Number(a.flipTick),
2918
- maker: lc(a.maker),
2919
- orderId: a.orderId.toString(),
2920
- side: a.isBid ? 'bid' : 'ask',
2921
- signer,
2922
- sourceToken: token(a.amount, a.token),
2923
- tick: Number(a.tick),
2924
- },
2925
- type: 'order-flipped',
2926
- },
2927
- ];
2928
- }
2929
- const pairCreated = findEvent('PairCreated');
2930
- if (pairCreated) {
2931
- const a = pairCreated.args;
2932
- return [
2933
- {
2934
- ...itemBase(logIndexOfEvent(pairCreated)),
2935
- data: { base: lc(a.base), key: hx(a.key), quote: lc(a.quote), signer },
2936
- type: 'pair-created',
2937
- },
2938
- ];
2939
- }
2940
- // TIP-20 token configuration / admin.
2941
- const pauseStateUpdate = findEvent('PauseStateUpdate');
2942
- if (pauseStateUpdate) {
2943
- const a = pauseStateUpdate.args;
2944
- return [
2945
- {
2946
- ...itemBase(logIndexOfEvent(pauseStateUpdate)),
2947
- data: { paused: a.isPaused, signer, updater: lc(a.updater) },
2948
- type: 'token-pause-set',
3470
+ recognize(named('SpendingLimitUpdated'), (event) => {
3471
+ const args = event.args;
3472
+ return {
3473
+ ...itemBase(event.logIndex),
3474
+ data: {
3475
+ account: lc(args.account),
3476
+ publicKey: lc(args.publicKey),
3477
+ signer,
3478
+ sourceToken: token(args.newLimit, args.token),
2949
3479
  },
2950
- ];
2951
- }
2952
- const supplyCapUpdate = findEvent('SupplyCapUpdate');
2953
- if (supplyCapUpdate) {
2954
- const a = supplyCapUpdate.args;
2955
- return [
2956
- {
2957
- ...itemBase(logIndexOfEvent(supplyCapUpdate)),
2958
- data: { signer, supplyCap: a.newSupplyCap.toString(), updater: lc(a.updater) },
2959
- type: 'token-supply-cap-set',
3480
+ type: 'spending-limit-updated',
3481
+ };
3482
+ });
3483
+ // Access-key lifecycle events.
3484
+ recognize((event) => event.eventName === 'KeyAuthorized' &&
3485
+ Address.isEqual(event.address, Addresses.accountKeychain), (event) => {
3486
+ const args = event.args;
3487
+ return {
3488
+ ...itemBase(event.logIndex),
3489
+ data: {
3490
+ account: lc(args.account),
3491
+ expiry: Number(args.expiry),
3492
+ publicKey: lc(args.publicKey),
3493
+ signatureType: Number(args.signatureType),
2960
3494
  },
2961
- ];
2962
- }
2963
- const transferPolicyUpdate = findEvent('TransferPolicyUpdate');
2964
- if (transferPolicyUpdate) {
2965
- const a = transferPolicyUpdate.args;
2966
- return [
2967
- {
2968
- ...itemBase(logIndexOfEvent(transferPolicyUpdate)),
2969
- data: { policyId: a.newPolicyId.toString(), signer, updater: lc(a.updater) },
2970
- type: 'token-transfer-policy-set',
3495
+ type: 'access-key-created',
3496
+ };
3497
+ });
3498
+ recognize((event) => event.eventName === 'KeyRevoked' && Address.isEqual(event.address, Addresses.accountKeychain), (event) => {
3499
+ const args = event.args;
3500
+ return {
3501
+ ...itemBase(event.logIndex),
3502
+ data: {
3503
+ account: lc(args.account),
3504
+ publicKey: lc(args.publicKey),
2971
3505
  },
2972
- ];
2973
- }
2974
- const quoteTokenUpdate = findEvent('QuoteTokenUpdate');
2975
- if (quoteTokenUpdate) {
2976
- const a = quoteTokenUpdate.args;
2977
- return [
2978
- {
2979
- ...itemBase(logIndexOfEvent(quoteTokenUpdate)),
2980
- data: { quoteToken: lc(a.newQuoteToken), signer, updater: lc(a.updater) },
2981
- type: 'token-quote-token-set',
3506
+ type: 'access-key-revoked',
3507
+ };
3508
+ });
3509
+ // Token creation. Factory initialization (role grants, config) precedes the
3510
+ // creation event; claim those, leaving later admin actions on the token as
3511
+ // independent activities.
3512
+ recognize((event) => event.eventName === 'TokenCreated' && Address.isEqual(event.address, Addresses.tip20Factory), (event) => {
3513
+ const args = event.args;
3514
+ unclaimed((e) => e.logIndex < event.logIndex && Address.isEqual(e.address, args.token)).forEach(claim);
3515
+ return {
3516
+ ...itemBase(event.logIndex),
3517
+ data: {
3518
+ address: lc(args.token),
3519
+ currency: args.currency,
3520
+ name: args.name,
3521
+ symbol: args.symbol,
2982
3522
  },
2983
- ];
2984
- }
2985
- const nextQuoteTokenSet = findEvent('NextQuoteTokenSet');
2986
- if (nextQuoteTokenSet) {
2987
- const a = nextQuoteTokenSet.args;
2988
- return [
2989
- {
2990
- ...itemBase(logIndexOfEvent(nextQuoteTokenSet)),
2991
- data: { nextQuoteToken: lc(a.nextQuoteToken), signer, updater: lc(a.updater) },
2992
- type: 'token-next-quote-token-set',
3523
+ type: 'token-created',
3524
+ };
3525
+ });
3526
+ // Governance / admin / DEX-bookkeeping events. Each maps a single decoded
3527
+ // event to a typed item; every remaining occurrence emits. These carry no
3528
+ // token movement, so the event itself is the whole signal.
3529
+ recognize(named('OrderFilled'), (event) => {
3530
+ const a = event.args;
3531
+ return {
3532
+ ...itemBase(event.logIndex),
3533
+ data: {
3534
+ amountFilled: a.amountFilled.toString(),
3535
+ maker: lc(a.maker),
3536
+ orderId: a.orderId.toString(),
3537
+ partialFill: a.partialFill,
3538
+ signer,
3539
+ taker: lc(a.taker),
2993
3540
  },
2994
- ];
2995
- }
2996
- const logoUriUpdated = findEvent('LogoURIUpdated');
2997
- if (logoUriUpdated) {
2998
- const a = logoUriUpdated.args;
2999
- return [
3000
- {
3001
- ...itemBase(logIndexOfEvent(logoUriUpdated)),
3002
- data: { logoUri: a.newLogoURI, signer, updater: lc(a.updater) },
3003
- type: 'token-logo-set',
3541
+ type: 'order-filled',
3542
+ };
3543
+ });
3544
+ recognize(named('OrderFlipped'), (event) => {
3545
+ const a = event.args;
3546
+ return {
3547
+ ...itemBase(event.logIndex),
3548
+ data: {
3549
+ flipTick: Number(a.flipTick),
3550
+ maker: lc(a.maker),
3551
+ orderId: a.orderId.toString(),
3552
+ side: a.isBid ? 'bid' : 'ask',
3553
+ signer,
3554
+ sourceToken: token(a.amount, a.token),
3555
+ tick: Number(a.tick),
3004
3556
  },
3005
- ];
3006
- }
3007
- const roleMembershipUpdated = findEvent('RoleMembershipUpdated');
3008
- if (roleMembershipUpdated) {
3009
- const a = roleMembershipUpdated.args;
3010
- return [
3011
- {
3012
- ...itemBase(logIndexOfEvent(roleMembershipUpdated)),
3013
- data: {
3014
- account: lc(a.account),
3015
- granted: a.hasRole,
3016
- role: hx(a.role),
3017
- sender: lc(a.sender),
3018
- signer,
3019
- },
3020
- type: 'role-membership-set',
3557
+ type: 'order-flipped',
3558
+ };
3559
+ });
3560
+ recognize(named('PairCreated'), (event) => {
3561
+ const a = event.args;
3562
+ return {
3563
+ ...itemBase(event.logIndex),
3564
+ data: { base: lc(a.base), key: hx(a.key), quote: lc(a.quote), signer },
3565
+ type: 'pair-created',
3566
+ };
3567
+ });
3568
+ // TIP-20 token configuration / admin.
3569
+ recognize(named('PauseStateUpdate'), (event) => {
3570
+ const a = event.args;
3571
+ return {
3572
+ ...itemBase(event.logIndex),
3573
+ data: { paused: a.isPaused, signer, updater: lc(a.updater) },
3574
+ type: 'token-pause-set',
3575
+ };
3576
+ });
3577
+ recognize(named('SupplyCapUpdate'), (event) => {
3578
+ const a = event.args;
3579
+ return {
3580
+ ...itemBase(event.logIndex),
3581
+ data: { signer, supplyCap: a.newSupplyCap.toString(), updater: lc(a.updater) },
3582
+ type: 'token-supply-cap-set',
3583
+ };
3584
+ });
3585
+ recognize(named('TransferPolicyUpdate'), (event) => {
3586
+ const a = event.args;
3587
+ return {
3588
+ ...itemBase(event.logIndex),
3589
+ data: { policyId: a.newPolicyId.toString(), signer, updater: lc(a.updater) },
3590
+ type: 'token-transfer-policy-set',
3591
+ };
3592
+ });
3593
+ recognize(named('QuoteTokenUpdate'), (event) => {
3594
+ const a = event.args;
3595
+ return {
3596
+ ...itemBase(event.logIndex),
3597
+ data: { quoteToken: lc(a.newQuoteToken), signer, updater: lc(a.updater) },
3598
+ type: 'token-quote-token-set',
3599
+ };
3600
+ });
3601
+ recognize(named('NextQuoteTokenSet'), (event) => {
3602
+ const a = event.args;
3603
+ return {
3604
+ ...itemBase(event.logIndex),
3605
+ data: { nextQuoteToken: lc(a.nextQuoteToken), signer, updater: lc(a.updater) },
3606
+ type: 'token-next-quote-token-set',
3607
+ };
3608
+ });
3609
+ recognize(named('LogoURIUpdated'), (event) => {
3610
+ const a = event.args;
3611
+ return {
3612
+ ...itemBase(event.logIndex),
3613
+ data: { logoUri: a.newLogoURI, signer, updater: lc(a.updater) },
3614
+ type: 'token-logo-set',
3615
+ };
3616
+ });
3617
+ recognize(named('RoleMembershipUpdated'), (event) => {
3618
+ const a = event.args;
3619
+ return {
3620
+ ...itemBase(event.logIndex),
3621
+ data: {
3622
+ account: lc(a.account),
3623
+ granted: a.hasRole,
3624
+ role: hx(a.role),
3625
+ sender: lc(a.sender),
3626
+ signer,
3021
3627
  },
3022
- ];
3023
- }
3024
- const roleAdminUpdated = findEvent('RoleAdminUpdated');
3025
- if (roleAdminUpdated) {
3026
- const a = roleAdminUpdated.args;
3027
- return [
3028
- {
3029
- ...itemBase(logIndexOfEvent(roleAdminUpdated)),
3030
- data: {
3031
- newAdminRole: hx(a.newAdminRole),
3032
- role: hx(a.role),
3033
- sender: lc(a.sender),
3034
- signer,
3035
- },
3036
- type: 'role-admin-set',
3628
+ type: 'role-membership-set',
3629
+ };
3630
+ });
3631
+ recognize(named('RoleAdminUpdated'), (event) => {
3632
+ const a = event.args;
3633
+ return {
3634
+ ...itemBase(event.logIndex),
3635
+ data: {
3636
+ newAdminRole: hx(a.newAdminRole),
3637
+ role: hx(a.role),
3638
+ sender: lc(a.sender),
3639
+ signer,
3037
3640
  },
3038
- ];
3039
- }
3641
+ type: 'role-admin-set',
3642
+ };
3643
+ });
3040
3644
  // Fee-manager token configuration.
3041
- const userTokenSet = findEvent('UserTokenSet');
3042
- if (userTokenSet) {
3043
- const a = userTokenSet.args;
3044
- return [
3045
- {
3046
- ...itemBase(logIndexOfEvent(userTokenSet)),
3047
- data: { signer, token: lc(a.token), user: lc(a.user) },
3048
- type: 'fee-user-token-set',
3049
- },
3050
- ];
3051
- }
3052
- const validatorTokenSet = findEvent('ValidatorTokenSet');
3053
- if (validatorTokenSet) {
3054
- const a = validatorTokenSet.args;
3055
- return [
3056
- {
3057
- ...itemBase(logIndexOfEvent(validatorTokenSet)),
3058
- data: { signer, token: lc(a.token), validator: lc(a.validator) },
3059
- type: 'fee-validator-token-set',
3060
- },
3061
- ];
3062
- }
3063
- // TIP-403 policy registry (create wins over the member events it bundles).
3064
- const compoundPolicyCreated = findEvent('CompoundPolicyCreated');
3065
- if (compoundPolicyCreated) {
3066
- const a = compoundPolicyCreated.args;
3067
- return [
3068
- {
3069
- ...itemBase(logIndexOfEvent(compoundPolicyCreated)),
3070
- data: {
3071
- creator: lc(a.creator),
3072
- mintRecipientPolicyId: a.mintRecipientPolicyId.toString(),
3073
- policyId: a.policyId.toString(),
3074
- recipientPolicyId: a.recipientPolicyId.toString(),
3075
- senderPolicyId: a.senderPolicyId.toString(),
3076
- signer,
3077
- },
3078
- type: 'compound-policy-created',
3645
+ recognize(named('UserTokenSet'), (event) => {
3646
+ const a = event.args;
3647
+ return {
3648
+ ...itemBase(event.logIndex),
3649
+ data: { signer, token: lc(a.token), user: lc(a.user) },
3650
+ type: 'fee-user-token-set',
3651
+ };
3652
+ });
3653
+ recognize(named('ValidatorTokenSet'), (event) => {
3654
+ const a = event.args;
3655
+ return {
3656
+ ...itemBase(event.logIndex),
3657
+ data: { signer, token: lc(a.token), validator: lc(a.validator) },
3658
+ type: 'fee-validator-token-set',
3659
+ };
3660
+ });
3661
+ // TIP-403 policy registry. Creation events claim the member events they
3662
+ // bundle (which precede the anchor); later member updates stay independent.
3663
+ recognize(named('CompoundPolicyCreated'), (event) => {
3664
+ const a = event.args;
3665
+ const componentIds = new Set([a.mintRecipientPolicyId, a.recipientPolicyId, a.senderPolicyId].map(String));
3666
+ unclaimed((e) => e.logIndex < event.logIndex &&
3667
+ Address.isEqual(e.address, event.address) &&
3668
+ ['BlacklistUpdated', 'PolicyAdminUpdated', 'PolicyCreated', 'WhitelistUpdated'].includes(e.eventName) &&
3669
+ 'policyId' in e.args &&
3670
+ componentIds.has(String(e.args.policyId))).forEach(claim);
3671
+ return {
3672
+ ...itemBase(event.logIndex),
3673
+ data: {
3674
+ creator: lc(a.creator),
3675
+ mintRecipientPolicyId: a.mintRecipientPolicyId.toString(),
3676
+ policyId: a.policyId.toString(),
3677
+ recipientPolicyId: a.recipientPolicyId.toString(),
3678
+ senderPolicyId: a.senderPolicyId.toString(),
3679
+ signer,
3079
3680
  },
3080
- ];
3081
- }
3082
- const policyCreated = findEvent('PolicyCreated');
3083
- if (policyCreated) {
3084
- const a = policyCreated.args;
3085
- return [
3086
- {
3087
- ...itemBase(logIndexOfEvent(policyCreated)),
3088
- data: {
3089
- policyId: a.policyId.toString(),
3090
- policyType: Number(a.policyType),
3091
- signer,
3092
- updater: lc(a.updater),
3093
- },
3094
- type: 'policy-created',
3681
+ type: 'compound-policy-created',
3682
+ };
3683
+ });
3684
+ recognize(named('PolicyCreated'), (event) => {
3685
+ const a = event.args;
3686
+ unclaimed((e) => e.logIndex < event.logIndex &&
3687
+ Address.isEqual(e.address, event.address) &&
3688
+ ['BlacklistUpdated', 'PolicyAdminUpdated', 'WhitelistUpdated'].includes(e.eventName) &&
3689
+ 'policyId' in e.args &&
3690
+ e.args.policyId === a.policyId).forEach(claim);
3691
+ return {
3692
+ ...itemBase(event.logIndex),
3693
+ data: {
3694
+ policyId: a.policyId.toString(),
3695
+ policyType: Number(a.policyType),
3696
+ signer,
3697
+ updater: lc(a.updater),
3095
3698
  },
3096
- ];
3097
- }
3098
- const policyAdminUpdated = findEvent('PolicyAdminUpdated');
3099
- if (policyAdminUpdated) {
3100
- const a = policyAdminUpdated.args;
3101
- return [
3102
- {
3103
- ...itemBase(logIndexOfEvent(policyAdminUpdated)),
3104
- data: {
3105
- admin: lc(a.admin),
3106
- policyId: a.policyId.toString(),
3107
- signer,
3108
- updater: lc(a.updater),
3109
- },
3110
- type: 'policy-admin-set',
3699
+ type: 'policy-created',
3700
+ };
3701
+ });
3702
+ recognize(named('PolicyAdminUpdated'), (event) => {
3703
+ const a = event.args;
3704
+ return {
3705
+ ...itemBase(event.logIndex),
3706
+ data: {
3707
+ admin: lc(a.admin),
3708
+ policyId: a.policyId.toString(),
3709
+ signer,
3710
+ updater: lc(a.updater),
3111
3711
  },
3112
- ];
3113
- }
3114
- const whitelistUpdated = findEvent('WhitelistUpdated');
3115
- if (whitelistUpdated) {
3116
- const a = whitelistUpdated.args;
3117
- return [
3118
- {
3119
- ...itemBase(logIndexOfEvent(whitelistUpdated)),
3120
- data: {
3121
- account: lc(a.account),
3122
- allowed: a.allowed,
3123
- policyId: a.policyId.toString(),
3124
- signer,
3125
- updater: lc(a.updater),
3126
- },
3127
- type: 'whitelist-updated',
3712
+ type: 'policy-admin-set',
3713
+ };
3714
+ });
3715
+ recognize(named('WhitelistUpdated'), (event) => {
3716
+ const a = event.args;
3717
+ return {
3718
+ ...itemBase(event.logIndex),
3719
+ data: {
3720
+ account: lc(a.account),
3721
+ allowed: a.allowed,
3722
+ policyId: a.policyId.toString(),
3723
+ signer,
3724
+ updater: lc(a.updater),
3128
3725
  },
3129
- ];
3130
- }
3131
- const blacklistUpdated = findEvent('BlacklistUpdated');
3132
- if (blacklistUpdated) {
3133
- const a = blacklistUpdated.args;
3134
- return [
3135
- {
3136
- ...itemBase(logIndexOfEvent(blacklistUpdated)),
3137
- data: {
3138
- account: lc(a.account),
3139
- policyId: a.policyId.toString(),
3140
- restricted: a.restricted,
3141
- signer,
3142
- updater: lc(a.updater),
3143
- },
3144
- type: 'blacklist-updated',
3726
+ type: 'whitelist-updated',
3727
+ };
3728
+ });
3729
+ recognize(named('BlacklistUpdated'), (event) => {
3730
+ const a = event.args;
3731
+ return {
3732
+ ...itemBase(event.logIndex),
3733
+ data: {
3734
+ account: lc(a.account),
3735
+ policyId: a.policyId.toString(),
3736
+ restricted: a.restricted,
3737
+ signer,
3738
+ updater: lc(a.updater),
3145
3739
  },
3146
- ];
3147
- }
3740
+ type: 'blacklist-updated',
3741
+ };
3742
+ });
3148
3743
  // Address / nonce registry.
3149
- const masterRegistered = findEvent('MasterRegistered');
3150
- if (masterRegistered) {
3151
- const a = masterRegistered.args;
3152
- return [
3153
- {
3154
- ...itemBase(logIndexOfEvent(masterRegistered)),
3155
- data: { masterAddress: lc(a.masterAddress), masterId: hx(a.masterId), signer },
3156
- type: 'master-registered',
3157
- },
3158
- ];
3159
- }
3160
- const nonceIncremented = findEvent('NonceIncremented');
3161
- if (nonceIncremented) {
3162
- const a = nonceIncremented.args;
3163
- return [
3164
- {
3165
- ...itemBase(logIndexOfEvent(nonceIncremented)),
3166
- data: {
3167
- account: lc(a.account),
3168
- nonce: a.newNonce.toString(),
3169
- nonceKey: a.nonceKey.toString(),
3170
- signer,
3171
- },
3172
- type: 'nonce-incremented',
3744
+ recognize(named('MasterRegistered'), (event) => {
3745
+ const a = event.args;
3746
+ return {
3747
+ ...itemBase(event.logIndex),
3748
+ data: { masterAddress: lc(a.masterAddress), masterId: hx(a.masterId), signer },
3749
+ type: 'master-registered',
3750
+ };
3751
+ });
3752
+ recognize(named('NonceIncremented'), (event) => {
3753
+ const a = event.args;
3754
+ return {
3755
+ ...itemBase(event.logIndex),
3756
+ data: {
3757
+ account: lc(a.account),
3758
+ nonce: a.newNonce.toString(),
3759
+ nonceKey: a.nonceKey.toString(),
3760
+ signer,
3173
3761
  },
3174
- ];
3175
- }
3762
+ type: 'nonce-incremented',
3763
+ };
3764
+ });
3176
3765
  // Access-key authorization witnesses.
3177
- const witnessRecorded = findEvent('KeyAuthorizationWitness');
3178
- if (witnessRecorded) {
3179
- const a = witnessRecorded.args;
3180
- return [
3181
- {
3182
- ...itemBase(logIndexOfEvent(witnessRecorded)),
3183
- data: { account: lc(a.account), signer, witness: hx(a.witness) },
3184
- type: 'key-authorization-witness',
3185
- },
3186
- ];
3187
- }
3188
- const witnessBurned = findEvent('KeyAuthorizationWitnessBurned');
3189
- if (witnessBurned) {
3190
- const a = witnessBurned.args;
3191
- return [
3192
- {
3193
- ...itemBase(logIndexOfEvent(witnessBurned)),
3194
- data: { account: lc(a.account), signer, witness: hx(a.witness) },
3195
- type: 'key-authorization-witness-burned',
3196
- },
3197
- ];
3198
- }
3199
- // Validator configuration (rotation supersedes the add/deactivate it bundles).
3200
- const validatorRotated = findEvent('ValidatorRotated');
3201
- if (validatorRotated) {
3202
- const a = validatorRotated.args;
3203
- return [
3204
- {
3205
- ...itemBase(logIndexOfEvent(validatorRotated)),
3206
- data: {
3207
- caller: lc(a.caller),
3208
- deactivatedIndex: a.deactivatedIndex.toString(),
3209
- egress: a.egress,
3210
- index: a.index.toString(),
3211
- ingress: a.ingress,
3212
- newPublicKey: hx(a.newPublicKey),
3213
- oldPublicKey: hx(a.oldPublicKey),
3214
- signer,
3215
- validator: lc(a.validatorAddress),
3216
- },
3217
- type: 'validator-rotated',
3218
- },
3219
- ];
3220
- }
3221
- const validatorAdded = findEvent('ValidatorAdded');
3222
- if (validatorAdded) {
3223
- const a = validatorAdded.args;
3224
- return [
3225
- {
3226
- ...itemBase(logIndexOfEvent(validatorAdded)),
3227
- data: {
3228
- egress: a.egress,
3229
- feeRecipient: lc(a.feeRecipient),
3230
- index: a.index.toString(),
3231
- ingress: a.ingress,
3232
- publicKey: hx(a.publicKey),
3233
- signer,
3234
- validator: lc(a.validatorAddress),
3235
- },
3236
- type: 'validator-added',
3237
- },
3238
- ];
3239
- }
3240
- const validatorDeactivated = findEvent('ValidatorDeactivated');
3241
- if (validatorDeactivated) {
3242
- const a = validatorDeactivated.args;
3243
- return [
3244
- {
3245
- ...itemBase(logIndexOfEvent(validatorDeactivated)),
3246
- data: { index: a.index.toString(), signer, validator: lc(a.validatorAddress) },
3247
- type: 'validator-deactivated',
3248
- },
3249
- ];
3250
- }
3251
- const validatorMigrated = findEvent('ValidatorMigrated');
3252
- if (validatorMigrated) {
3253
- const a = validatorMigrated.args;
3254
- return [
3255
- {
3256
- ...itemBase(logIndexOfEvent(validatorMigrated)),
3257
- data: {
3258
- index: a.index.toString(),
3259
- publicKey: hx(a.publicKey),
3260
- signer,
3261
- validator: lc(a.validatorAddress),
3262
- },
3263
- type: 'validator-migrated',
3766
+ recognize(named('KeyAuthorizationWitness'), (event) => {
3767
+ const a = event.args;
3768
+ return {
3769
+ ...itemBase(event.logIndex),
3770
+ data: { account: lc(a.account), signer, witness: hx(a.witness) },
3771
+ type: 'key-authorization-witness',
3772
+ };
3773
+ });
3774
+ recognize(named('KeyAuthorizationWitnessBurned'), (event) => {
3775
+ const a = event.args;
3776
+ return {
3777
+ ...itemBase(event.logIndex),
3778
+ data: { account: lc(a.account), signer, witness: hx(a.witness) },
3779
+ type: 'key-authorization-witness-burned',
3780
+ };
3781
+ });
3782
+ // Validator configuration. Rotation claims the add/deactivate pair it
3783
+ // bundles, matched by validator index.
3784
+ recognize(named('ValidatorRotated'), (event) => {
3785
+ const a = event.args;
3786
+ unclaimed((e) => {
3787
+ if (!('index' in e.args))
3788
+ return false;
3789
+ const index = e.args.index;
3790
+ return ((e.eventName === 'ValidatorAdded' && index === a.index) ||
3791
+ (e.eventName === 'ValidatorDeactivated' && index === a.deactivatedIndex));
3792
+ }).forEach(claim);
3793
+ return {
3794
+ ...itemBase(event.logIndex),
3795
+ data: {
3796
+ caller: lc(a.caller),
3797
+ deactivatedIndex: a.deactivatedIndex.toString(),
3798
+ egress: a.egress,
3799
+ index: a.index.toString(),
3800
+ ingress: a.ingress,
3801
+ newPublicKey: hx(a.newPublicKey),
3802
+ oldPublicKey: hx(a.oldPublicKey),
3803
+ signer,
3804
+ validator: lc(a.validatorAddress),
3264
3805
  },
3265
- ];
3266
- }
3267
- const validatorMigrationSkipped = findEvent('SkippedValidatorMigration');
3268
- if (validatorMigrationSkipped) {
3269
- const a = validatorMigrationSkipped.args;
3270
- return [
3271
- {
3272
- ...itemBase(logIndexOfEvent(validatorMigrationSkipped)),
3273
- data: {
3274
- index: a.index.toString(),
3275
- publicKey: hx(a.publicKey),
3276
- signer,
3277
- validator: lc(a.validatorAddress),
3278
- },
3279
- type: 'validator-migration-skipped',
3806
+ type: 'validator-rotated',
3807
+ };
3808
+ });
3809
+ recognize(named('ValidatorAdded'), (event) => {
3810
+ const a = event.args;
3811
+ return {
3812
+ ...itemBase(event.logIndex),
3813
+ data: {
3814
+ egress: a.egress,
3815
+ feeRecipient: lc(a.feeRecipient),
3816
+ index: a.index.toString(),
3817
+ ingress: a.ingress,
3818
+ publicKey: hx(a.publicKey),
3819
+ signer,
3820
+ validator: lc(a.validatorAddress),
3280
3821
  },
3281
- ];
3282
- }
3283
- const feeRecipientUpdated = findEvent('FeeRecipientUpdated');
3284
- if (feeRecipientUpdated) {
3285
- const a = feeRecipientUpdated.args;
3286
- return [
3287
- {
3288
- ...itemBase(logIndexOfEvent(feeRecipientUpdated)),
3289
- data: {
3290
- caller: lc(a.caller),
3291
- feeRecipient: lc(a.feeRecipient),
3292
- index: a.index.toString(),
3293
- signer,
3294
- },
3295
- type: 'validator-fee-recipient-set',
3822
+ type: 'validator-added',
3823
+ };
3824
+ });
3825
+ recognize(named('ValidatorDeactivated'), (event) => {
3826
+ const a = event.args;
3827
+ return {
3828
+ ...itemBase(event.logIndex),
3829
+ data: { index: a.index.toString(), signer, validator: lc(a.validatorAddress) },
3830
+ type: 'validator-deactivated',
3831
+ };
3832
+ });
3833
+ recognize(named('ValidatorMigrated'), (event) => {
3834
+ const a = event.args;
3835
+ return {
3836
+ ...itemBase(event.logIndex),
3837
+ data: {
3838
+ index: a.index.toString(),
3839
+ publicKey: hx(a.publicKey),
3840
+ signer,
3841
+ validator: lc(a.validatorAddress),
3296
3842
  },
3297
- ];
3298
- }
3299
- const ipAddressesUpdated = findEvent('IpAddressesUpdated');
3300
- if (ipAddressesUpdated) {
3301
- const a = ipAddressesUpdated.args;
3302
- return [
3303
- {
3304
- ...itemBase(logIndexOfEvent(ipAddressesUpdated)),
3305
- data: {
3306
- caller: lc(a.caller),
3307
- egress: a.egress,
3308
- index: a.index.toString(),
3309
- ingress: a.ingress,
3310
- signer,
3311
- },
3312
- type: 'validator-ip-set',
3843
+ type: 'validator-migrated',
3844
+ };
3845
+ });
3846
+ recognize(named('SkippedValidatorMigration'), (event) => {
3847
+ const a = event.args;
3848
+ return {
3849
+ ...itemBase(event.logIndex),
3850
+ data: {
3851
+ index: a.index.toString(),
3852
+ publicKey: hx(a.publicKey),
3853
+ signer,
3854
+ validator: lc(a.validatorAddress),
3313
3855
  },
3314
- ];
3315
- }
3316
- const validatorOwnershipTransferred = findEvent('ValidatorOwnershipTransferred');
3317
- if (validatorOwnershipTransferred) {
3318
- const a = validatorOwnershipTransferred.args;
3319
- return [
3320
- {
3321
- ...itemBase(logIndexOfEvent(validatorOwnershipTransferred)),
3322
- data: {
3323
- caller: lc(a.caller),
3324
- index: a.index.toString(),
3325
- newAddress: lc(a.newAddress),
3326
- oldAddress: lc(a.oldAddress),
3327
- signer,
3328
- },
3329
- type: 'validator-ownership-transferred',
3856
+ type: 'validator-migration-skipped',
3857
+ };
3858
+ });
3859
+ recognize(named('FeeRecipientUpdated'), (event) => {
3860
+ const a = event.args;
3861
+ return {
3862
+ ...itemBase(event.logIndex),
3863
+ data: {
3864
+ caller: lc(a.caller),
3865
+ feeRecipient: lc(a.feeRecipient),
3866
+ index: a.index.toString(),
3867
+ signer,
3330
3868
  },
3331
- ];
3332
- }
3333
- const ownershipTransferred = findEvent('OwnershipTransferred');
3334
- if (ownershipTransferred) {
3335
- const a = ownershipTransferred.args;
3336
- return [
3337
- {
3338
- ...itemBase(logIndexOfEvent(ownershipTransferred)),
3339
- data: { newOwner: lc(a.newOwner), oldOwner: lc(a.oldOwner), signer },
3340
- type: 'ownership-transferred',
3869
+ type: 'validator-fee-recipient-set',
3870
+ };
3871
+ });
3872
+ recognize(named('IpAddressesUpdated'), (event) => {
3873
+ const a = event.args;
3874
+ return {
3875
+ ...itemBase(event.logIndex),
3876
+ data: {
3877
+ caller: lc(a.caller),
3878
+ egress: a.egress,
3879
+ index: a.index.toString(),
3880
+ ingress: a.ingress,
3881
+ signer,
3341
3882
  },
3342
- ];
3343
- }
3344
- const rotationEpochSet = findEvent('NetworkIdentityRotationEpochSet');
3345
- if (rotationEpochSet) {
3346
- const a = rotationEpochSet.args;
3347
- return [
3348
- {
3349
- ...itemBase(logIndexOfEvent(rotationEpochSet)),
3350
- data: {
3351
- nextEpoch: a.nextEpoch.toString(),
3352
- previousEpoch: a.previousEpoch.toString(),
3353
- signer,
3354
- },
3355
- type: 'network-identity-rotation-epoch-set',
3883
+ type: 'validator-ip-set',
3884
+ };
3885
+ });
3886
+ recognize(named('ValidatorOwnershipTransferred'), (event) => {
3887
+ const a = event.args;
3888
+ return {
3889
+ ...itemBase(event.logIndex),
3890
+ data: {
3891
+ caller: lc(a.caller),
3892
+ index: a.index.toString(),
3893
+ newAddress: lc(a.newAddress),
3894
+ oldAddress: lc(a.oldAddress),
3895
+ signer,
3356
3896
  },
3357
- ];
3358
- }
3359
- const initialized = findEvent('Initialized');
3360
- if (initialized) {
3361
- const a = initialized.args;
3362
- return [
3363
- {
3364
- ...itemBase(logIndexOfEvent(initialized)),
3365
- data: { height: a.height.toString(), signer },
3366
- type: 'initialized',
3897
+ type: 'validator-ownership-transferred',
3898
+ };
3899
+ });
3900
+ recognize(named('OwnershipTransferred'), (event) => {
3901
+ const a = event.args;
3902
+ return {
3903
+ ...itemBase(event.logIndex),
3904
+ data: { newOwner: lc(a.newOwner), oldOwner: lc(a.oldOwner), signer },
3905
+ type: 'ownership-transferred',
3906
+ };
3907
+ });
3908
+ recognize(named('NetworkIdentityRotationEpochSet'), (event) => {
3909
+ const a = event.args;
3910
+ return {
3911
+ ...itemBase(event.logIndex),
3912
+ data: {
3913
+ nextEpoch: a.nextEpoch.toString(),
3914
+ previousEpoch: a.previousEpoch.toString(),
3915
+ signer,
3367
3916
  },
3368
- ];
3369
- }
3370
- // Default: plain transfer (excluding transfers to the fee manager).
3371
- const userTransfers = transfers.filter((e) => !Address.isEqual(e.args.to, Addresses.feeManager));
3372
- if (userTransfers.length > 0) {
3373
- const t = userTransfers[0];
3374
- const args = t.args;
3375
- const to = args.to.toLowerCase();
3376
- const from = args.from.toLowerCase();
3377
- const rawMemo = t.eventName === 'TransferWithMemo' ? args.memo : undefined;
3917
+ type: 'network-identity-rotation-epoch-set',
3918
+ };
3919
+ });
3920
+ recognize(named('Initialized'), (event) => {
3921
+ const a = event.args;
3922
+ return {
3923
+ ...itemBase(event.logIndex),
3924
+ data: { height: a.height.toString(), signer },
3925
+ type: 'initialized',
3926
+ };
3927
+ });
3928
+ // Plain transfers: every remaining viewer-party transfer is its own
3929
+ // activity. Non-viewer transfers are internal legs of what classified
3930
+ // above and surface only via the fallback below.
3931
+ const buildTransfer = (event) => {
3932
+ const args = transferArgs(event);
3933
+ const to = lc(args.to);
3934
+ const from = lc(args.from);
3935
+ const rawMemo = event.eventName === 'TransferWithMemo' ? args.memo : undefined;
3378
3936
  // The raw memo (not the suppressed display string) carries the MPP
3379
3937
  // attribution fingerprint, so resolve the service name from it.
3380
3938
  const memo = rawMemo ? Transfers.memoToString(rawMemo) : undefined;
3381
3939
  const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo });
3382
- const sourceToken = token(args.amount, t.address);
3940
+ const sourceToken = token(args.amount, event.address);
3383
3941
  if (Address.isEqual(to, viewer))
3384
- return [
3385
- transfer(itemBase(logIndexOfEvent(t)), blockNum, {
3386
- attribution,
3387
- direction: 'in',
3388
- memo,
3389
- recipient: viewer,
3390
- sender: from,
3391
- sourceToken,
3392
- }),
3393
- ...accessKeyItems,
3394
- ];
3395
- return [
3396
- transfer(itemBase(logIndexOfEvent(t)), blockNum, {
3942
+ return transfer(itemBase(event.logIndex), blockNum, {
3397
3943
  attribution,
3398
- direction: 'out',
3944
+ direction: 'in',
3399
3945
  memo,
3400
- recipient: to,
3946
+ recipient: viewer,
3401
3947
  sender: from,
3402
- signer,
3403
3948
  sourceToken,
3404
- }),
3405
- ...accessKeyItems,
3406
- ];
3949
+ });
3950
+ return transfer(itemBase(event.logIndex), blockNum, {
3951
+ attribution,
3952
+ direction: 'out',
3953
+ memo,
3954
+ recipient: to,
3955
+ sender: from,
3956
+ signer,
3957
+ sourceToken,
3958
+ });
3959
+ };
3960
+ const isUserTransfer = (event) => isTransfer(event) && !Address.isEqual(transferArgs(event).to, Addresses.feeManager);
3961
+ recognize((event) => isUserTransfer(event) &&
3962
+ (Address.isEqual(transferArgs(event).from, viewer) ||
3963
+ Address.isEqual(transferArgs(event).to, viewer)), buildTransfer);
3964
+ // Fallback when nothing viewer-facing classified: surface fold-only vault
3965
+ // items, third-party approvals, and transfers (e.g. a sponsored batch)
3966
+ // instead of reporting `unknown`.
3967
+ if (items.length === 0) {
3968
+ items.push(...suppressedVaultItems);
3969
+ recognize(isErc20Approval, approvalItem);
3970
+ recognize(isUserTransfer, buildTransfer);
3407
3971
  }
3408
- // Pure access-key tx (no transfer / mint / burn / etc.).
3409
- if (accessKeyItems.length > 0)
3410
- return accessKeyItems;
3411
- // Unclassified: a tx the address sent or paid for whose logs matched no known
3412
- // classifier. Surface it honestly as an `unknown` item carrying every log
3413
- // (decoded when possible), rather than fabricating a zero transfer.
3414
- return [unknown(itemBase(minLogIndex), { events: buildTxEvents(rows, events), signer })];
3415
- }
3416
- /** Extracts the block-wide log index from a decoded viem event. */
3417
- function logIndexOfEvent(event) {
3418
- return event.logIndex;
3972
+ // Unclassified: a tx the address sent or paid for whose logs matched no
3973
+ // known classifier. Surface it honestly as an `unknown` item carrying every
3974
+ // log (decoded when possible), rather than fabricating a zero transfer.
3975
+ if (items.length === 0)
3976
+ return [unknown(itemBase(minLogIndex), { events: buildTxEvents(rows, events), signer })];
3977
+ return items.sort((a, b) => a.logIndex - b.logIndex);
3419
3978
  }
3420
3979
  /**
3421
3980
  * Builds a `transfer` item whose `data` is the `Transfer` API resource plus the
@@ -3454,29 +4013,31 @@ function unknown(base, data) {
3454
4013
  }
3455
4014
  /** Decodes a group's raw rows into `viem/tempo` events via `parseEventLogs` (non-strict). */
3456
4015
  function decodeRows(rows) {
3457
- return rows.flatMap((r) => {
3458
- const log = {
3459
- address: r.logAddress,
3460
- blockHash: '0x0',
3461
- blockNumber: BigInt(r.blockNum),
3462
- data: (r.data ?? '0x'),
3463
- logIndex: r.logIdx,
3464
- removed: false,
3465
- topics: [r.selector, r.topic1, r.topic2, r.topic3].filter(Boolean),
3466
- transactionHash: r.txHash,
3467
- transactionIndex: r.txIdx,
3468
- };
3469
- try {
3470
- return parseEventLogs({
3471
- abi: [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault],
3472
- logs: [log],
3473
- strict: false,
3474
- });
3475
- }
3476
- catch {
3477
- return [];
3478
- }
3479
- });
4016
+ const logs = rows.map((r) => ({
4017
+ address: r.logAddress,
4018
+ blockHash: '0x0',
4019
+ blockNumber: BigInt(r.blockNum),
4020
+ data: (r.data ?? '0x'),
4021
+ logIndex: r.logIdx,
4022
+ removed: false,
4023
+ topics: [r.selector, r.topic1, r.topic2, r.topic3].filter(Boolean),
4024
+ transactionHash: r.txHash,
4025
+ transactionIndex: r.txIdx,
4026
+ }));
4027
+ try {
4028
+ // One batched call: viem re-processes the merged ABI per invocation, so
4029
+ // per-row calls cost ~55x more. Non-strict decoding skips undecodable
4030
+ // logs per log and can yield `args: undefined`; normalize so `in`-guards
4031
+ // never throw.
4032
+ return parseEventLogs({
4033
+ abi: [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault, ...erc4626Events],
4034
+ logs,
4035
+ strict: false,
4036
+ }).map((event) => ({ ...event, args: event.args ?? {} }));
4037
+ }
4038
+ catch {
4039
+ return [];
4040
+ }
3480
4041
  }
3481
4042
  /**
3482
4043
  * Builds the full per-transaction `events` capture: one entry per raw log,
@@ -3578,12 +4139,26 @@ async function enrichTokens(c, options) {
3578
4139
  collectTokens(item, addresses);
3579
4140
  const includeLogoUri = include.includes('token.logoUri');
3580
4141
  const includeVerified = include.includes('token.verified');
3581
- // Curated data is needed only for requested valuation or token fields.
3582
- const snapshot = addresses.size > 0 && (denomination !== undefined || includeLogoUri || includeVerified)
3583
- ? await VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
4142
+ const resolved_promise = addresses.size > 0
4143
+ ? Timing.time(c, 'tokens', () => Promise.all([...addresses].map(async (address) => {
4144
+ const [logoUri, metadata] = await Promise.all([
4145
+ includeLogoUri
4146
+ ? Tokens.getTokenLogo(c, { address, chainId }).catch(() => undefined)
4147
+ : undefined,
4148
+ getActivityTokenMetadata(c, { address, chainId }),
4149
+ ]);
4150
+ return { address: address.toLowerCase(), logoUri, metadata };
4151
+ })))
3584
4152
  : undefined;
3585
- const rates = denomination
3586
- ? await Valuation.ratesFor(c, {
4153
+ // Curated data is needed only for requested valuation or token fields.
4154
+ const snapshot_promise = addresses.size > 0 && (denomination !== undefined || includeLogoUri || includeVerified)
4155
+ ? VerifiedTokens.snapshot(c, chainId).catch(() => undefined)
4156
+ : Promise.resolve(undefined);
4157
+ const rates_promise = (async () => {
4158
+ if (!denomination)
4159
+ return undefined;
4160
+ const snapshot = await snapshot_promise;
4161
+ return Valuation.ratesFor(c, {
3587
4162
  currencies: snapshot
3588
4163
  ? [...addresses].flatMap((address) => {
3589
4164
  const held = snapshot.byAddress.get(address)?.currency;
@@ -3592,37 +4167,63 @@ async function enrichTokens(c, options) {
3592
4167
  : [],
3593
4168
  denomination,
3594
4169
  oracle,
3595
- })
3596
- : undefined;
3597
- if (addresses.size === 0)
3598
- return { items: items, rates };
3599
- const resolved = await Timing.time(c, 'tokens', () => Promise.all([...addresses].map(async (address) => {
3600
- const [logoUri, metadata] = await Promise.all([
3601
- includeLogoUri
3602
- ? Tokens.getTokenLogo(c, { address, chainId }).catch(() => undefined)
4170
+ });
4171
+ })();
4172
+ if (!resolved_promise)
4173
+ return { items: items, rates: await rates_promise };
4174
+ const [resolved_raw, rates, snapshot] = await Promise.all([
4175
+ resolved_promise,
4176
+ rates_promise,
4177
+ snapshot_promise,
4178
+ ]);
4179
+ const resolved = resolved_raw.map(({ address, logoUri, metadata }) => [
4180
+ address,
4181
+ {
4182
+ currency: metadata.currency,
4183
+ decimals: metadata.decimals,
4184
+ logoUri: includeLogoUri
4185
+ ? (logoUri ?? snapshot?.byAddress.get(address)?.logoUri ?? metadata.logoUri)
3603
4186
  : undefined,
3604
- getActivityTokenMetadata(c, { address, chainId }),
3605
- ]);
3606
- return [
3607
- address.toLowerCase(),
3608
- {
3609
- currency: metadata.currency,
3610
- decimals: metadata.decimals,
3611
- logoUri: includeLogoUri
3612
- ? (logoUri ?? snapshot?.byAddress.get(address)?.logoUri ?? metadata.logoUri)
3613
- : undefined,
3614
- name: metadata.name,
3615
- symbol: metadata.symbol,
3616
- verified: includeVerified && snapshot ? snapshot.byAddress.has(address) : undefined,
3617
- },
3618
- ];
3619
- })));
4187
+ name: metadata.name,
4188
+ symbol: metadata.symbol,
4189
+ verified: includeVerified && snapshot ? snapshot.byAddress.has(address) : undefined,
4190
+ },
4191
+ ]);
3620
4192
  const byAddress = new Map(resolved);
3621
4193
  return {
3622
4194
  items: items.map((item) => enrichItem(item, byAddress, { denomination, rates, snapshot })),
3623
4195
  rates,
3624
4196
  };
3625
4197
  }
4198
+ /** Resolves token metadata on each item's source chain without changing feed order. */
4199
+ async function enrichChainTokens(c, options) {
4200
+ const byChain = new Map();
4201
+ for (const item of options.items) {
4202
+ const items = byChain.get(item.chainId) ?? [];
4203
+ items.push(item);
4204
+ byChain.set(item.chainId, items);
4205
+ }
4206
+ const results = await Promise.all([...byChain].map(async ([chainId, items]) => ({
4207
+ chainId,
4208
+ ...(await enrichTokens(c, {
4209
+ chainId,
4210
+ denomination: options.denomination,
4211
+ include: options.include,
4212
+ items,
4213
+ oracle: options.oracle,
4214
+ })),
4215
+ })));
4216
+ const buckets = new Map();
4217
+ for (const result of results)
4218
+ buckets.set(result.chainId, { index: 0, items: result.items });
4219
+ return {
4220
+ items: options.items.map((item) => {
4221
+ const bucket = buckets.get(item.chainId);
4222
+ return bucket.items[bucket.index++];
4223
+ }),
4224
+ rates: results.find((result) => result.rates !== undefined)?.rates,
4225
+ };
4226
+ }
3626
4227
  /** Returns an enriched copy of an item without mutating its cached token stubs. */
3627
4228
  function enrichItem(item, byAddress, valuation) {
3628
4229
  const source = item.data;
@@ -3680,6 +4281,7 @@ function groupByAccessKeyItems(items) {
3680
4281
  const itemSigner = signerOf(item);
3681
4282
  if (itemSigner !== 'self') {
3682
4283
  if (currentGroup &&
4284
+ currentGroup.items[0].chainId === item.chainId &&
3683
4285
  Address.isEqual(currentGroup.signer, itemSigner) &&
3684
4286
  sameCalendarDay(currentGroup.items[0].timestamp, item.timestamp)) {
3685
4287
  currentGroup.items.push(item);
@@ -3717,6 +4319,7 @@ function flushGroup(group) {
3717
4319
  const firstItem = group.items[0];
3718
4320
  const lastItem = group.items[group.items.length - 1];
3719
4321
  return {
4322
+ ...(firstItem.chainId === undefined ? {} : { chainId: firstItem.chainId }),
3720
4323
  data: { items: group.items, signer: group.signer },
3721
4324
  perspective: firstItem.perspective,
3722
4325
  id: `${firstItem.id}..${lastItem.id}:${group.items.length}`,