tapimo 0.0.0 → 0.0.2

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 (422) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -0
  3. package/dist/ApiKey.d.ts +65 -0
  4. package/dist/ApiKey.d.ts.map +1 -0
  5. package/dist/ApiKey.js +105 -0
  6. package/dist/ApiKey.js.map +1 -0
  7. package/dist/ApiKeySource.d.ts +166 -0
  8. package/dist/ApiKeySource.d.ts.map +1 -0
  9. package/dist/ApiKeySource.js +234 -0
  10. package/dist/ApiKeySource.js.map +1 -0
  11. package/dist/App.d.ts +19317 -0
  12. package/dist/App.d.ts.map +1 -0
  13. package/dist/App.js +612 -0
  14. package/dist/App.js.map +1 -0
  15. package/dist/Assets.d.ts +44 -0
  16. package/dist/Assets.d.ts.map +1 -0
  17. package/dist/Assets.js +23 -0
  18. package/dist/Assets.js.map +1 -0
  19. package/dist/Auth.d.ts +2 -0
  20. package/dist/Auth.d.ts.map +1 -0
  21. package/dist/Auth.js +2 -0
  22. package/dist/Auth.js.map +1 -0
  23. package/dist/Cli.d.ts +21 -0
  24. package/dist/Cli.d.ts.map +1 -0
  25. package/dist/Cli.js +13 -0
  26. package/dist/Cli.js.map +1 -0
  27. package/dist/Client.d.ts +20 -0
  28. package/dist/Client.d.ts.map +1 -0
  29. package/dist/Client.js +24 -0
  30. package/dist/Client.js.map +1 -0
  31. package/dist/Metrics.d.ts +70 -0
  32. package/dist/Metrics.d.ts.map +1 -0
  33. package/dist/Metrics.js +91 -0
  34. package/dist/Metrics.js.map +1 -0
  35. package/dist/Scope.d.ts +35 -0
  36. package/dist/Scope.d.ts.map +1 -0
  37. package/dist/Scope.js +38 -0
  38. package/dist/Scope.js.map +1 -0
  39. package/dist/Webhooks.d.ts +79 -0
  40. package/dist/Webhooks.d.ts.map +1 -0
  41. package/dist/Webhooks.js +107 -0
  42. package/dist/Webhooks.js.map +1 -0
  43. package/dist/admin/Access.d.ts +41 -0
  44. package/dist/admin/Access.d.ts.map +1 -0
  45. package/dist/admin/Access.js +75 -0
  46. package/dist/admin/Access.js.map +1 -0
  47. package/dist/admin/App.d.ts +1221 -0
  48. package/dist/admin/App.d.ts.map +1 -0
  49. package/dist/admin/App.js +113 -0
  50. package/dist/admin/App.js.map +1 -0
  51. package/dist/admin/apps/api-keys.d.ts +209 -0
  52. package/dist/admin/apps/api-keys.d.ts.map +1 -0
  53. package/dist/admin/apps/api-keys.js +129 -0
  54. package/dist/admin/apps/api-keys.js.map +1 -0
  55. package/dist/admin/apps/scopes.d.ts +46 -0
  56. package/dist/admin/apps/scopes.d.ts.map +1 -0
  57. package/dist/admin/apps/scopes.js +41 -0
  58. package/dist/admin/apps/scopes.js.map +1 -0
  59. package/dist/admin/apps/verified-tokens.d.ts +1206 -0
  60. package/dist/admin/apps/verified-tokens.d.ts.map +1 -0
  61. package/dist/admin/apps/verified-tokens.js +485 -0
  62. package/dist/admin/apps/verified-tokens.js.map +1 -0
  63. package/dist/admin/index.d.ts +3 -0
  64. package/dist/admin/index.d.ts.map +1 -0
  65. package/dist/admin/index.js +3 -0
  66. package/dist/admin/index.js.map +1 -0
  67. package/dist/admin/ui.generated.d.ts +11 -0
  68. package/dist/admin/ui.generated.d.ts.map +1 -0
  69. package/dist/admin/ui.generated.js +43 -0
  70. package/dist/admin/ui.generated.js.map +1 -0
  71. package/dist/apps/activities.d.ts +18725 -0
  72. package/dist/apps/activities.d.ts.map +1 -0
  73. package/dist/apps/activities.js +3138 -0
  74. package/dist/apps/activities.js.map +1 -0
  75. package/dist/apps/addresses.d.ts +3662 -0
  76. package/dist/apps/addresses.d.ts.map +1 -0
  77. package/dist/apps/addresses.js +22 -0
  78. package/dist/apps/addresses.js.map +1 -0
  79. package/dist/apps/balances.d.ts +342 -0
  80. package/dist/apps/balances.d.ts.map +1 -0
  81. package/dist/apps/balances.js +393 -0
  82. package/dist/apps/balances.js.map +1 -0
  83. package/dist/apps/blocks.d.ts +931 -0
  84. package/dist/apps/blocks.d.ts.map +1 -0
  85. package/dist/apps/blocks.js +689 -0
  86. package/dist/apps/blocks.js.map +1 -0
  87. package/dist/apps/coingecko.d.ts +1053 -0
  88. package/dist/apps/coingecko.d.ts.map +1 -0
  89. package/dist/apps/coingecko.js +1015 -0
  90. package/dist/apps/coingecko.js.map +1 -0
  91. package/dist/apps/exchanges.d.ts +3000 -0
  92. package/dist/apps/exchanges.d.ts.map +1 -0
  93. package/dist/apps/exchanges.js +3570 -0
  94. package/dist/apps/exchanges.js.map +1 -0
  95. package/dist/apps/fee-amm.d.ts +640 -0
  96. package/dist/apps/fee-amm.d.ts.map +1 -0
  97. package/dist/apps/fee-amm.js +815 -0
  98. package/dist/apps/fee-amm.js.map +1 -0
  99. package/dist/apps/indexer.d.ts +116 -0
  100. package/dist/apps/indexer.d.ts.map +1 -0
  101. package/dist/apps/indexer.js +270 -0
  102. package/dist/apps/indexer.js.map +1 -0
  103. package/dist/apps/receipts.d.ts +1438 -0
  104. package/dist/apps/receipts.d.ts.map +1 -0
  105. package/dist/apps/receipts.js +874 -0
  106. package/dist/apps/receipts.js.map +1 -0
  107. package/dist/apps/rpc.d.ts +173 -0
  108. package/dist/apps/rpc.d.ts.map +1 -0
  109. package/dist/apps/rpc.js +191 -0
  110. package/dist/apps/rpc.js.map +1 -0
  111. package/dist/apps/tokenlist.d.ts +192 -0
  112. package/dist/apps/tokenlist.d.ts.map +1 -0
  113. package/dist/apps/tokenlist.js +153 -0
  114. package/dist/apps/tokenlist.js.map +1 -0
  115. package/dist/apps/tokens.d.ts +2246 -0
  116. package/dist/apps/tokens.d.ts.map +1 -0
  117. package/dist/apps/tokens.js +1789 -0
  118. package/dist/apps/tokens.js.map +1 -0
  119. package/dist/apps/transactions.d.ts +2327 -0
  120. package/dist/apps/transactions.d.ts.map +1 -0
  121. package/dist/apps/transactions.js +975 -0
  122. package/dist/apps/transactions.js.map +1 -0
  123. package/dist/apps/transfers.d.ts +589 -0
  124. package/dist/apps/transfers.d.ts.map +1 -0
  125. package/dist/apps/transfers.js +769 -0
  126. package/dist/apps/transfers.js.map +1 -0
  127. package/dist/apps/verified-tokens.d.ts +544 -0
  128. package/dist/apps/verified-tokens.d.ts.map +1 -0
  129. package/dist/apps/verified-tokens.js +234 -0
  130. package/dist/apps/verified-tokens.js.map +1 -0
  131. package/dist/apps/webhooks.d.ts +7684 -0
  132. package/dist/apps/webhooks.d.ts.map +1 -0
  133. package/dist/apps/webhooks.js +2112 -0
  134. package/dist/apps/webhooks.js.map +1 -0
  135. package/dist/cloudflare.d.ts +32 -0
  136. package/dist/cloudflare.d.ts.map +1 -0
  137. package/dist/cloudflare.js +45 -0
  138. package/dist/cloudflare.js.map +1 -0
  139. package/dist/index.d.ts +16 -0
  140. package/dist/index.d.ts.map +1 -0
  141. package/dist/index.js +16 -0
  142. package/dist/index.js.map +1 -0
  143. package/dist/internal/Auth.d.ts +298 -0
  144. package/dist/internal/Auth.d.ts.map +1 -0
  145. package/dist/internal/Auth.js +618 -0
  146. package/dist/internal/Auth.js.map +1 -0
  147. package/dist/internal/Cache.d.ts +74 -0
  148. package/dist/internal/Cache.d.ts.map +1 -0
  149. package/dist/internal/Cache.js +215 -0
  150. package/dist/internal/Cache.js.map +1 -0
  151. package/dist/internal/Cursor.d.ts +79 -0
  152. package/dist/internal/Cursor.d.ts.map +1 -0
  153. package/dist/internal/Cursor.js +116 -0
  154. package/dist/internal/Cursor.js.map +1 -0
  155. package/dist/internal/EdgeCache.d.ts +59 -0
  156. package/dist/internal/EdgeCache.d.ts.map +1 -0
  157. package/dist/internal/EdgeCache.js +174 -0
  158. package/dist/internal/EdgeCache.js.map +1 -0
  159. package/dist/internal/Log.d.ts +104 -0
  160. package/dist/internal/Log.d.ts.map +1 -0
  161. package/dist/internal/Log.js +118 -0
  162. package/dist/internal/Log.js.map +1 -0
  163. package/dist/internal/MetricSink.d.ts +65 -0
  164. package/dist/internal/MetricSink.d.ts.map +1 -0
  165. package/dist/internal/MetricSink.js +146 -0
  166. package/dist/internal/MetricSink.js.map +1 -0
  167. package/dist/internal/Mpp.d.ts +71 -0
  168. package/dist/internal/Mpp.d.ts.map +1 -0
  169. package/dist/internal/Mpp.js +105 -0
  170. package/dist/internal/Mpp.js.map +1 -0
  171. package/dist/internal/OpenApi.d.ts +313 -0
  172. package/dist/internal/OpenApi.d.ts.map +1 -0
  173. package/dist/internal/OpenApi.fonts.d.ts +2 -0
  174. package/dist/internal/OpenApi.fonts.d.ts.map +1 -0
  175. package/dist/internal/OpenApi.fonts.js +39 -0
  176. package/dist/internal/OpenApi.fonts.js.map +1 -0
  177. package/dist/internal/OpenApi.js +335 -0
  178. package/dist/internal/OpenApi.js.map +1 -0
  179. package/dist/internal/OpenApi.logo.d.ts +6 -0
  180. package/dist/internal/OpenApi.logo.d.ts.map +1 -0
  181. package/dist/internal/OpenApi.logo.js +19 -0
  182. package/dist/internal/OpenApi.logo.js.map +1 -0
  183. package/dist/internal/Path.d.ts +5 -0
  184. package/dist/internal/Path.d.ts.map +1 -0
  185. package/dist/internal/Path.js +16 -0
  186. package/dist/internal/Path.js.map +1 -0
  187. package/dist/internal/RateLimit.d.ts +60 -0
  188. package/dist/internal/RateLimit.d.ts.map +1 -0
  189. package/dist/internal/RateLimit.js +48 -0
  190. package/dist/internal/RateLimit.js.map +1 -0
  191. package/dist/internal/Response.d.ts +132 -0
  192. package/dist/internal/Response.d.ts.map +1 -0
  193. package/dist/internal/Response.js +72 -0
  194. package/dist/internal/Response.js.map +1 -0
  195. package/dist/internal/Schema.d.ts +257 -0
  196. package/dist/internal/Schema.d.ts.map +1 -0
  197. package/dist/internal/Schema.js +360 -0
  198. package/dist/internal/Schema.js.map +1 -0
  199. package/dist/internal/Store.d.ts +204 -0
  200. package/dist/internal/Store.d.ts.map +1 -0
  201. package/dist/internal/Store.js +291 -0
  202. package/dist/internal/Store.js.map +1 -0
  203. package/dist/internal/Tidx.d.ts +120 -0
  204. package/dist/internal/Tidx.d.ts.map +1 -0
  205. package/dist/internal/Tidx.js +156 -0
  206. package/dist/internal/Tidx.js.map +1 -0
  207. package/dist/internal/Timing.d.ts +22 -0
  208. package/dist/internal/Timing.d.ts.map +1 -0
  209. package/dist/internal/Timing.js +32 -0
  210. package/dist/internal/Timing.js.map +1 -0
  211. package/dist/internal/Ttl.d.ts +25 -0
  212. package/dist/internal/Ttl.d.ts.map +1 -0
  213. package/dist/internal/Ttl.js +49 -0
  214. package/dist/internal/Ttl.js.map +1 -0
  215. package/dist/internal/Value.d.ts +47 -0
  216. package/dist/internal/Value.d.ts.map +1 -0
  217. package/dist/internal/Value.js +122 -0
  218. package/dist/internal/Value.js.map +1 -0
  219. package/dist/internal/VerifiedTokens.d.ts +228 -0
  220. package/dist/internal/VerifiedTokens.d.ts.map +1 -0
  221. package/dist/internal/VerifiedTokens.js +450 -0
  222. package/dist/internal/VerifiedTokens.js.map +1 -0
  223. package/dist/internal/Viem.d.ts +2082 -0
  224. package/dist/internal/Viem.d.ts.map +1 -0
  225. package/dist/internal/Viem.js +63 -0
  226. package/dist/internal/Viem.js.map +1 -0
  227. package/dist/internal/WebhookDestination.d.ts +198 -0
  228. package/dist/internal/WebhookDestination.d.ts.map +1 -0
  229. package/dist/internal/WebhookDestination.js +170 -0
  230. package/dist/internal/WebhookDestination.js.map +1 -0
  231. package/dist/internal/Webhooks.d.ts +433 -0
  232. package/dist/internal/Webhooks.d.ts.map +1 -0
  233. package/dist/internal/Webhooks.js +489 -0
  234. package/dist/internal/Webhooks.js.map +1 -0
  235. package/dist/internal/webhookDestinations/betterStack.d.ts +26 -0
  236. package/dist/internal/webhookDestinations/betterStack.d.ts.map +1 -0
  237. package/dist/internal/webhookDestinations/betterStack.js +76 -0
  238. package/dist/internal/webhookDestinations/betterStack.js.map +1 -0
  239. package/dist/internal/webhookDestinations/slack.d.ts +59 -0
  240. package/dist/internal/webhookDestinations/slack.d.ts.map +1 -0
  241. package/dist/internal/webhookDestinations/slack.js +422 -0
  242. package/dist/internal/webhookDestinations/slack.js.map +1 -0
  243. package/dist/internal/webhookDestinations/url.d.ts +17 -0
  244. package/dist/internal/webhookDestinations/url.d.ts.map +1 -0
  245. package/dist/internal/webhookDestinations/url.js +130 -0
  246. package/dist/internal/webhookDestinations/url.js.map +1 -0
  247. package/dist/openrpc.json +5302 -0
  248. package/package.json +46 -4
  249. package/seeds/31318/icons/0x20c0000000000000000000000000000000000000.svg +1 -0
  250. package/seeds/31318/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
  251. package/seeds/31318/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
  252. package/seeds/31318/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
  253. package/seeds/31318/verified-tokens.json +30 -0
  254. package/seeds/4217/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
  255. package/seeds/4217/icons/0x20c000000000000000000000048c8f36df1c9a4a.svg +1 -0
  256. package/seeds/4217/icons/0x20c0000000000000000000000520792dcccccccc.svg +1 -0
  257. package/seeds/4217/icons/0x20c0000000000000000000000a6da882d075a4c3.svg +4 -0
  258. package/seeds/4217/icons/0x20c000000000000000000000111111111e910f0f.svg +30 -0
  259. package/seeds/4217/icons/0x20c00000000000000000000014f22ca97301eb73.svg +1 -0
  260. package/seeds/4217/icons/0x20c0000000000000000000001621e21f71cf12fb.svg +1 -0
  261. package/seeds/4217/icons/0x20c0000000000000000000002f52d5cc21a3207b.svg +1 -0
  262. package/seeds/4217/icons/0x20c0000000000000000000003158081efd85bfc2.svg +11 -0
  263. package/seeds/4217/icons/0x20c0000000000000000000003554d28269e0f3c2.svg +1 -0
  264. package/seeds/4217/icons/0x20c000000000000000000000383a23bacb546ab9.svg +1 -0
  265. package/seeds/4217/icons/0x20c00000000000000000000042109aef2f8b28e1.png +0 -0
  266. package/seeds/4217/icons/0x20c0000000000000000000005c0bac7cef389a11.svg +1 -0
  267. package/seeds/4217/icons/0x20c0000000000000000000006fd9a167923ba194.png +0 -0
  268. package/seeds/4217/icons/0x20c0000000000000000000007f7ba549dd0251b9.svg +1 -0
  269. package/seeds/4217/icons/0x20c0000000000000000000008191667423f70e67.svg +5 -0
  270. package/seeds/4217/icons/0x20c0000000000000000000008ee4fcff88888888.svg +1 -0
  271. package/seeds/4217/icons/0x20c0000000000000000000009a4a4b17e0dc6651.svg +1 -0
  272. package/seeds/4217/icons/0x20c000000000000000000000ab02d39df30bd17e.svg +1 -0
  273. package/seeds/4217/icons/0x20c000000000000000000000ae247a1130450f09.svg +1 -0
  274. package/seeds/4217/icons/0x20c000000000000000000000aeed2ec36a54d0e5.svg +1 -0
  275. package/seeds/4217/icons/0x20c000000000000000000000b9537d11c60e8b50.svg +1 -0
  276. package/seeds/4217/icons/0x20c000000000000000000000bd95bfb69fbe6ce3.svg +1 -0
  277. package/seeds/4217/icons/0x20c000000000000000000000c412ec89d0c08be5.svg +1 -0
  278. package/seeds/4217/icons/0x20c000000000000000000000f047dd7018e50367.png +0 -0
  279. package/seeds/4217/verified-tokens.json +177 -0
  280. package/seeds/42431/icons/0x20c0000000000000000000000000000000000000.svg +25 -0
  281. package/seeds/42431/icons/0x20c0000000000000000000000000000000000001.svg +1 -0
  282. package/seeds/42431/icons/0x20c0000000000000000000000000000000000002.svg +1 -0
  283. package/seeds/42431/icons/0x20c0000000000000000000000000000000000003.svg +1 -0
  284. package/seeds/42431/icons/0x20c0000000000000000000009e8d7eb59b783726.svg +1 -0
  285. package/seeds/42431/icons/0x20c000000000000000000000d72572838bbee59c.svg +1 -0
  286. package/seeds/42431/verified-tokens.json +44 -0
  287. package/seeds/openrpc/README.md +27 -0
  288. package/seeds/openrpc/execution-apis.json +8989 -0
  289. package/src/ApiKey.test.ts +91 -0
  290. package/src/ApiKey.ts +122 -0
  291. package/src/ApiKeySource.test-d.ts +11 -0
  292. package/src/ApiKeySource.test.ts +295 -0
  293. package/src/ApiKeySource.ts +303 -0
  294. package/src/App.test-d.ts +38 -0
  295. package/src/App.test.ts +65927 -0
  296. package/src/App.ts +855 -0
  297. package/src/Assets.ts +58 -0
  298. package/src/Auth.ts +1 -0
  299. package/src/Cli.test.ts +81 -0
  300. package/src/Cli.ts +38 -0
  301. package/src/Client.test-d.ts +1438 -0
  302. package/src/Client.test.ts +134 -0
  303. package/src/Client.ts +45 -0
  304. package/src/Metrics.ts +154 -0
  305. package/src/Scope.ts +50 -0
  306. package/src/Webhooks.test.ts +49 -0
  307. package/src/Webhooks.ts +172 -0
  308. package/src/admin/Access.test.ts +130 -0
  309. package/src/admin/Access.ts +87 -0
  310. package/src/admin/App.test.ts +225 -0
  311. package/src/admin/App.ts +216 -0
  312. package/src/admin/apps/api-keys.test.ts +156 -0
  313. package/src/admin/apps/api-keys.ts +157 -0
  314. package/src/admin/apps/scopes.test.ts +52 -0
  315. package/src/admin/apps/scopes.ts +47 -0
  316. package/src/admin/apps/verified-tokens.test.ts +415 -0
  317. package/src/admin/apps/verified-tokens.ts +566 -0
  318. package/src/admin/index.ts +2 -0
  319. package/src/admin/ui/plugin.ts +135 -0
  320. package/src/admin/ui/src/components/Checkbox.tsx +22 -0
  321. package/src/admin/ui/src/components/Dialog.tsx +32 -0
  322. package/src/admin/ui/src/components/MobileProfile.tsx +31 -0
  323. package/src/admin/ui/src/components/Page.tsx +53 -0
  324. package/src/admin/ui/src/components/TempoLogo.tsx +37 -0
  325. package/src/admin/ui/src/fonts/Pilat-Demi.woff2 +0 -0
  326. package/src/admin/ui/src/fonts/Pilat-Regular.woff2 +0 -0
  327. package/src/admin/ui/src/lib/api-keys.ts +63 -0
  328. package/src/admin/ui/src/lib/client.ts +15 -0
  329. package/src/admin/ui/src/lib/config.ts +57 -0
  330. package/src/admin/ui/src/lib/query-client.ts +26 -0
  331. package/src/admin/ui/src/lib/scopes.ts +25 -0
  332. package/src/admin/ui/src/lib/verified-tokens.ts +89 -0
  333. package/src/admin/ui/src/lib/wagmi.ts +18 -0
  334. package/src/admin/ui/src/routeTree.gen.ts +104 -0
  335. package/src/admin/ui/src/router.tsx +25 -0
  336. package/src/admin/ui/src/routes/__root.tsx +219 -0
  337. package/src/admin/ui/src/routes/api-keys.tsx +329 -0
  338. package/src/admin/ui/src/routes/index.tsx +7 -0
  339. package/src/admin/ui/src/routes/verified-tokens.tsx +331 -0
  340. package/src/admin/ui/src/styles.css +20 -0
  341. package/src/admin/ui/tsconfig.json +16 -0
  342. package/src/admin/ui/vite.config.ts +29 -0
  343. package/src/admin/ui.generated.ts +60 -0
  344. package/src/apps/activities.test.ts +1020 -0
  345. package/src/apps/activities.ts +3934 -0
  346. package/src/apps/addresses.ts +24 -0
  347. package/src/apps/balances.test.ts +274 -0
  348. package/src/apps/balances.ts +533 -0
  349. package/src/apps/blocks.test.ts +385 -0
  350. package/src/apps/blocks.ts +896 -0
  351. package/src/apps/coingecko.test.ts +218 -0
  352. package/src/apps/coingecko.ts +1383 -0
  353. package/src/apps/exchanges.test.ts +958 -0
  354. package/src/apps/exchanges.ts +4773 -0
  355. package/src/apps/fee-amm.test.ts +321 -0
  356. package/src/apps/fee-amm.ts +1121 -0
  357. package/src/apps/indexer.test.ts +153 -0
  358. package/src/apps/indexer.ts +330 -0
  359. package/src/apps/receipts.test.ts +285 -0
  360. package/src/apps/receipts.ts +1193 -0
  361. package/src/apps/rpc.test.ts +302 -0
  362. package/src/apps/rpc.ts +222 -0
  363. package/src/apps/tokenlist.test.ts +80 -0
  364. package/src/apps/tokenlist.ts +207 -0
  365. package/src/apps/tokens.test.ts +1062 -0
  366. package/src/apps/tokens.ts +2292 -0
  367. package/src/apps/transactions.test.ts +408 -0
  368. package/src/apps/transactions.ts +1363 -0
  369. package/src/apps/transfers.test.ts +371 -0
  370. package/src/apps/transfers.ts +1116 -0
  371. package/src/apps/verified-tokens.test.ts +114 -0
  372. package/src/apps/verified-tokens.ts +295 -0
  373. package/src/apps/webhooks.test.ts +1937 -0
  374. package/src/apps/webhooks.ts +2926 -0
  375. package/src/cloudflare.ts +55 -0
  376. package/src/index.ts +15 -0
  377. package/src/internal/Auth.test.ts +980 -0
  378. package/src/internal/Auth.ts +1012 -0
  379. package/src/internal/Cache.test.ts +240 -0
  380. package/src/internal/Cache.ts +262 -0
  381. package/src/internal/Cursor.test.ts +100 -0
  382. package/src/internal/Cursor.ts +149 -0
  383. package/src/internal/EdgeCache.test.ts +234 -0
  384. package/src/internal/EdgeCache.ts +224 -0
  385. package/src/internal/Errors.ts +14 -0
  386. package/src/internal/Log.test.ts +244 -0
  387. package/src/internal/Log.ts +216 -0
  388. package/src/internal/MetricSink.ts +220 -0
  389. package/src/internal/Mpp.test.ts +64 -0
  390. package/src/internal/Mpp.ts +141 -0
  391. package/src/internal/OpenApi.fonts.ts +38 -0
  392. package/src/internal/OpenApi.logo.ts +18 -0
  393. package/src/internal/OpenApi.ts +488 -0
  394. package/src/internal/Path.test.ts +14 -0
  395. package/src/internal/Path.ts +15 -0
  396. package/src/internal/RateLimit.test.ts +93 -0
  397. package/src/internal/RateLimit.ts +93 -0
  398. package/src/internal/Response.ts +131 -0
  399. package/src/internal/Schema.test.ts +34 -0
  400. package/src/internal/Schema.ts +523 -0
  401. package/src/internal/Store.test.ts +545 -0
  402. package/src/internal/Store.ts +526 -0
  403. package/src/internal/Tidx.local.test.ts +33 -0
  404. package/src/internal/Tidx.test.ts +294 -0
  405. package/src/internal/Tidx.ts +238 -0
  406. package/src/internal/Timing.test.ts +22 -0
  407. package/src/internal/Timing.ts +62 -0
  408. package/src/internal/Ttl.test.ts +18 -0
  409. package/src/internal/Ttl.ts +59 -0
  410. package/src/internal/Value.ts +114 -0
  411. package/src/internal/VerifiedTokens.test.ts +333 -0
  412. package/src/internal/VerifiedTokens.ts +645 -0
  413. package/src/internal/Viem.test.ts +115 -0
  414. package/src/internal/Viem.ts +165 -0
  415. package/src/internal/WebhookDestination.test.ts +648 -0
  416. package/src/internal/WebhookDestination.ts +331 -0
  417. package/src/internal/Webhooks.test.ts +668 -0
  418. package/src/internal/Webhooks.ts +940 -0
  419. package/src/internal/webhookDestinations/betterStack.ts +91 -0
  420. package/src/internal/webhookDestinations/slack.ts +496 -0
  421. package/src/internal/webhookDestinations/url.ts +128 -0
  422. package/src/openrpc.json +5302 -0
@@ -0,0 +1,3934 @@
1
+ import { Hono, type Context } from 'hono'
2
+ import { Address, Hex } from 'ox'
3
+ import { type Log, parseEventLogs, zeroAddress } from 'viem'
4
+ import { Abis, Addresses } from 'viem/tempo'
5
+ import * as z from 'zod/mini'
6
+
7
+ import type * as App from '../App.js'
8
+ import * as Auth from '../internal/Auth.js'
9
+ import * as Cache from '../internal/Cache.js'
10
+ import * as Cursor from '../internal/Cursor.js'
11
+ import * as Mpp from '../internal/Mpp.js'
12
+ import * as OpenApi from '../internal/OpenApi.js'
13
+ import * as Response from '../internal/Response.js'
14
+ import * as Schema from '../internal/Schema.js'
15
+ import * as Store from '../internal/Store.js'
16
+ import type * as Tidx from '../internal/Tidx.js'
17
+ import * as Timing from '../internal/Timing.js'
18
+ import * as Ttl from '../internal/Ttl.js'
19
+ import * as Value from '../internal/Value.js'
20
+ import * as Tokens from './tokens.js'
21
+ import * as Transfers from './transfers.js'
22
+
23
+ /** Example transaction hash surfaced in OpenAPI docs. */
24
+ const exampleHash = '0x4845ae2098724ab26a5d89370dce5124d044be6a5acc164afc348662de67d474'
25
+
26
+ /** Zod schemas owned by the activity handlers. */
27
+ export namespace schema {
28
+ /** A signer: the literal `self`, or the access-key address that signed. */
29
+ const Signer = z
30
+ .union([z.literal('self'), Schema.Address])
31
+ .check(
32
+ z.describe('Who signed the activity: `self`, or the access-key address used to sign it.'),
33
+ )
34
+
35
+ /**
36
+ * A token and the amount that moved, in the same shape as the API's transfer
37
+ * resource (`Transfers.schema.SourceToken`): a contract `address` and a
38
+ * decimal-integer `amount`, plus best-effort `decimals`/`symbol` metadata when
39
+ * the token has been resolved.
40
+ */
41
+ export const Token = Transfers.schema.SourceToken
42
+
43
+ /**
44
+ * A token transfer as classified by the feed: the `/transfers` row shape
45
+ * plus the feed-only, subject-relative fields (`direction`, `signer`) that
46
+ * the unscoped resource cannot carry, with `memo`/`attribution` always
47
+ * resolved (the feed already decodes the underlying events).
48
+ */
49
+ export const Transfer = z
50
+ .object({
51
+ attribution: z
52
+ .optional(z.string())
53
+ .check(
54
+ z.describe(
55
+ 'MPP service name matched from the transfer memo, when Tempo recognizes the fingerprint.',
56
+ ),
57
+ z.meta({ examples: ['Acme Payments'] }),
58
+ ),
59
+ blockNumber: z
60
+ .number()
61
+ .check(
62
+ z.int(),
63
+ z.nonnegative(),
64
+ z.describe('Block number where this transfer was included.'),
65
+ z.meta({ examples: [12345678] }),
66
+ ),
67
+ destinationToken: z
68
+ .optional(Token)
69
+ .check(
70
+ z.describe(
71
+ 'Token and amount delivered to the recipient when it differs from the source token — a cross-token transfer routed through the DEX. Omitted for a same-token transfer.',
72
+ ),
73
+ ),
74
+ direction: z
75
+ .enum(['in', 'out'])
76
+ .check(
77
+ z.describe(
78
+ 'The address’s side of the transfer: `in` means received, and `out` means sent.',
79
+ ),
80
+ ),
81
+ memo: z
82
+ .optional(z.string())
83
+ .check(z.describe('Text memo attached to the transfer, when one was provided.'), z.meta({ examples: ['Invoice #1234'] })), // prettier-ignore
84
+ recipient: Schema.Address.check(z.describe('Address that received the transfer.')),
85
+ sender: Schema.Address.check(z.describe('Address that sent the transfer.')),
86
+ signer: z
87
+ .optional(Signer)
88
+ .check(
89
+ z.describe(
90
+ 'For outbound transfers, who signed it: `self` or the access-key address. Omitted for inbound transfers.',
91
+ ),
92
+ ),
93
+ sourceToken: Token.check(z.describe('Token and amount that moved in the transfer.')),
94
+ timestamp: z
95
+ .iso.datetime()
96
+ .check(z.describe('Block timestamp for the transfer.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })), // prettier-ignore
97
+ transactionHash: Schema.Hash.check(
98
+ z.describe('Transaction hash that contains this transfer.'),
99
+ ),
100
+ })
101
+ .check(z.describe('One token transfer involving the requested address.'))
102
+
103
+ /**
104
+ * One transaction log, decoded into a known Tempo event when possible. The
105
+ * raw fields (`address`/`data`/`logIndex`/`topics`) are always present so no
106
+ * log is hidden; the decoded fields (`args`/`eventName`/`title`) are present
107
+ * only when the log matched a known `viem/tempo` ABI.
108
+ */
109
+ const event = z
110
+ .object({
111
+ address: Schema.Address.check(z.describe('Contract address that emitted the log.')),
112
+ args: z
113
+ .optional(z.record(z.string(), z.unknown()))
114
+ .check(
115
+ z.describe(
116
+ 'Decoded event arguments keyed by parameter name, present when the log matched a known ABI. Integer values are decimal strings.',
117
+ ),
118
+ ),
119
+ data: Schema.Hex.check(z.describe('Unindexed log data as a hex string (`0x` when empty).')),
120
+ eventName: z
121
+ .optional(z.string())
122
+ .check(z.describe('Event name decoded from the `viem/tempo` ABIs, present when the log matched a known ABI.'), z.meta({ examples: ['Transfer'] })), // prettier-ignore
123
+ logIndex: z
124
+ .number()
125
+ .check(z.int(), z.nonnegative(), z.describe('Log index for this log within the block.'), z.meta({ examples: [4] })), // prettier-ignore
126
+ title: z
127
+ .optional(z.string())
128
+ .check(z.describe('Human-readable label for the decoded event, present when the log matched a known ABI.'), z.meta({ examples: ['Token transferred'] })), // prettier-ignore
129
+ topics: z
130
+ .array(Schema.Hash)
131
+ .check(z.describe('Raw log topics; the first topic is the event signature hash.')),
132
+ })
133
+ .check(
134
+ z.describe(
135
+ 'One transaction log. Decoded fields (`eventName`/`args`/`title`) are present when it matched a known Tempo ABI; otherwise only the raw fields are set.',
136
+ ),
137
+ )
138
+
139
+ /**
140
+ * Fields shared by every activity item. `transactionHash`/`timestamp` identify and sort
141
+ * the entry; `data` carries the type-specific payload — shaped like the
142
+ * matching API resource where one exists, plus any feed-only fields (e.g. a
143
+ * transfer's `direction`/`memo`/`attribution`/`signer`) the resource lacks.
144
+ */
145
+ const base = {
146
+ perspective: z
147
+ .enum(['incoming', 'outgoing'])
148
+ .check(
149
+ z.describe(
150
+ '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.',
151
+ ),
152
+ ),
153
+ events: z
154
+ .optional(z.array(event))
155
+ .check(
156
+ z.describe(
157
+ '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.',
158
+ ),
159
+ ),
160
+ id: z.string().check(
161
+ z.describe(
162
+ 'Stable activity ID built from the transaction hash and log index (`${transactionHash}-${logIndex}`).',
163
+ ),
164
+ z.meta({
165
+ examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665-3'],
166
+ }),
167
+ ),
168
+ logIndex: z
169
+ .number()
170
+ .check(
171
+ z.int(),
172
+ z.nonnegative(),
173
+ z.describe('Log index for this activity within the block.'),
174
+ z.meta({ examples: [3] }),
175
+ ),
176
+ timestamp: z
177
+ .iso.datetime()
178
+ .check(z.describe('Block timestamp as an ISO 8601 string.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })), // prettier-ignore
179
+ title: z
180
+ .string()
181
+ .check(
182
+ z.describe(
183
+ '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`).',
184
+ ),
185
+ z.meta({ examples: ['Token transferred'] }),
186
+ ),
187
+ transactionHash: Schema.Hash.check(z.describe('Transaction hash for this activity.')),
188
+ }
189
+
190
+ /** The classified activity item variants (discriminated by `type`). */
191
+ export const items = [
192
+ z
193
+ .object({
194
+ ...base,
195
+ data: Transfer.check(
196
+ z.describe(
197
+ 'Transfer details, using the `/transfers` shape plus feed-only fields such as direction and signer.',
198
+ ),
199
+ ),
200
+ type: z
201
+ .literal('transfer')
202
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
203
+ })
204
+ .check(z.meta({ title: 'Token transferred' })),
205
+ z
206
+ .object({
207
+ ...base,
208
+ data: z
209
+ .object({
210
+ recipient: Schema.Address.check(z.describe('Address that received the transfer.')),
211
+ sourceToken: Token.check(z.describe('Token and amount that were minted.')),
212
+ })
213
+ .check(z.describe('A token mint activity.')),
214
+ type: z
215
+ .literal('mint')
216
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
217
+ })
218
+ .check(z.meta({ title: 'Token minted' })),
219
+ z
220
+ .object({
221
+ ...base,
222
+ data: z
223
+ .object({
224
+ sender: Schema.Address.check(z.describe('Address whose token balance was burned.')),
225
+ signer: Signer,
226
+ sourceToken: Token.check(z.describe('Token and amount that were burned.')),
227
+ })
228
+ .check(z.describe('A token burn activity.')),
229
+ type: z
230
+ .literal('burn')
231
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
232
+ })
233
+ .check(z.meta({ title: 'Token burned' })),
234
+ z
235
+ .object({
236
+ ...base,
237
+ data: z
238
+ .object({
239
+ destinationToken: Token.check(z.describe('Token the address received in the swap.')),
240
+ signer: Signer,
241
+ sourceToken: Token.check(z.describe('Token the address sent into the swap.')),
242
+ })
243
+ .check(
244
+ z.describe(
245
+ 'Swap inferred from token transfers through the DEX precompile. This feed entry is a summary, so per-fill details, route, and rate are not included.',
246
+ ),
247
+ ),
248
+ type: z
249
+ .literal('swap')
250
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
251
+ })
252
+ .check(z.meta({ title: 'Tokens swapped' })),
253
+ z
254
+ .object({
255
+ ...base,
256
+ data: z
257
+ .object({
258
+ signer: Signer,
259
+ sourceToken: Token.check(
260
+ z.describe('Token and allowance amount approved for spending.'),
261
+ ),
262
+ spender: Schema.Address.check(
263
+ z.describe('Address allowed to spend the approved amount.'),
264
+ ),
265
+ })
266
+ .check(z.describe('A token allowance approval activity.')),
267
+ type: z
268
+ .literal('approval')
269
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
270
+ })
271
+ .check(z.meta({ title: 'Allowance approved' })),
272
+ z
273
+ .object({
274
+ ...base,
275
+ data: z
276
+ .object({
277
+ refund: z
278
+ .optional(Token)
279
+ .check(z.describe('Token refunded to the address when the session closed, if any.')),
280
+ signer: Signer,
281
+ })
282
+ .check(z.describe('A payment-channel session close activity.')),
283
+ type: z
284
+ .literal('session-closed')
285
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
286
+ })
287
+ .check(z.meta({ title: 'Session closed' })),
288
+ z
289
+ .object({
290
+ ...base,
291
+ data: z
292
+ .object({
293
+ account: Schema.Address.check(
294
+ z.describe('Account this access key was authorized for.'),
295
+ ),
296
+ expiry: z
297
+ .number()
298
+ .check(z.int(), z.describe('Access-key expiration time as a Unix timestamp.'), z.meta({ examples: [1735689600] })), // prettier-ignore
299
+ publicKey: Schema.Address.check(
300
+ z.describe('Public key for the authorized access key.'),
301
+ ),
302
+ signatureType: z
303
+ .number()
304
+ .check(
305
+ z.int(),
306
+ z.describe('Signature type used by the access key.'),
307
+ z.meta({ examples: [0] }),
308
+ ),
309
+ })
310
+ .check(z.describe('An access key was created for the account.')),
311
+ type: z
312
+ .literal('access-key-created')
313
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
314
+ })
315
+ .check(z.meta({ title: 'Access key created' })),
316
+ z
317
+ .object({
318
+ ...base,
319
+ data: z
320
+ .object({
321
+ account: Schema.Address.check(z.describe('Account this access key was revoked from.')),
322
+ publicKey: Schema.Address.check(z.describe('Public key for the revoked access key.')),
323
+ })
324
+ .check(z.describe('An access key was revoked from the account.')),
325
+ type: z
326
+ .literal('access-key-revoked')
327
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
328
+ })
329
+ .check(z.meta({ title: 'Access key revoked' })),
330
+ z
331
+ .object({
332
+ ...base,
333
+ data: z
334
+ .object({
335
+ address: Schema.Address.check(z.describe('Contract address of the new TIP-20 token.')),
336
+ currency: z.string().check(z.describe('Display currency for the token, such as USD.'), z.meta({ examples: ['USD'] })), // prettier-ignore
337
+ name: z.string().check(z.describe('Human-readable token name.'), z.meta({ examples: ['Tempo USD'] })), // prettier-ignore
338
+ symbol: z
339
+ .string()
340
+ .check(z.describe('Token ticker symbol.'), z.meta({ examples: ['TUSD'] })),
341
+ })
342
+ .check(z.describe('A new TIP-20 token was created.')),
343
+ type: z
344
+ .literal('token-created')
345
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
346
+ })
347
+ .check(z.meta({ title: 'Token created' })),
348
+ z
349
+ .object({
350
+ ...base,
351
+ data: z
352
+ .object({
353
+ sender: Schema.Address.check(
354
+ z.describe('Address whose blocked balance was burned by policy.'),
355
+ ),
356
+ signer: Signer,
357
+ sourceToken: Token.check(z.describe('Token and amount that were burned.')),
358
+ })
359
+ .check(
360
+ z.describe(
361
+ 'A compliance burn of funds the receive policy blocked, distinct from a normal burn.',
362
+ ),
363
+ ),
364
+ type: z
365
+ .literal('burn-blocked')
366
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
367
+ })
368
+ .check(z.meta({ title: 'Burn blocked' })),
369
+ z
370
+ .object({
371
+ ...base,
372
+ data: z
373
+ .object({
374
+ channelId: Schema.Hex.check(z.describe('Identifier of the payment channel.')),
375
+ payee: Schema.Address.check(
376
+ z.describe('Address that receives payments from the channel.'),
377
+ ),
378
+ payer: Schema.Address.check(z.describe('Address that funds the channel.')),
379
+ signer: Signer,
380
+ sourceToken: Token.check(
381
+ z.describe('Token and deposit amount locked into the channel.'),
382
+ ),
383
+ })
384
+ .check(z.describe('A payment channel was opened and funded.')),
385
+ type: z
386
+ .literal('channel-opened')
387
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
388
+ })
389
+ .check(z.meta({ title: 'Channel opened' })),
390
+ z
391
+ .object({
392
+ ...base,
393
+ data: z
394
+ .object({
395
+ channelId: Schema.Hex.check(z.describe('Identifier of the payment channel.')),
396
+ signer: Signer,
397
+ sourceToken: Token.check(z.describe('Token and amount added to the channel deposit.')),
398
+ })
399
+ .check(z.describe('A payment channel deposit was topped up.')),
400
+ type: z
401
+ .literal('channel-funded')
402
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
403
+ })
404
+ .check(z.meta({ title: 'Channel funded' })),
405
+ z
406
+ .object({
407
+ ...base,
408
+ data: z
409
+ .object({
410
+ channelId: Schema.Hex.check(z.describe('Identifier of the payment channel.')),
411
+ payee: Schema.Address.check(z.describe('Address that received the settled payment.')),
412
+ payer: Schema.Address.check(z.describe('Address that funded the channel.')),
413
+ signer: Signer,
414
+ sourceToken: Token.check(z.describe('Token and amount paid out in this settlement.')),
415
+ })
416
+ .check(z.describe('An incremental payment-channel settlement to the payee.')),
417
+ type: z
418
+ .literal('channel-settled')
419
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
420
+ })
421
+ .check(z.meta({ title: 'Channel settled' })),
422
+ z
423
+ .object({
424
+ ...base,
425
+ data: z
426
+ .object({
427
+ channelId: Schema.Hex.check(z.describe('Identifier of the payment channel.')),
428
+ payee: Schema.Address.check(
429
+ z.describe('Address that receives payments from the channel.'),
430
+ ),
431
+ payer: Schema.Address.check(z.describe('Address that funded the channel.')),
432
+ refund: z
433
+ .optional(Token)
434
+ .check(z.describe('Token and amount refunded to the payer on close, if any.')),
435
+ signer: Signer,
436
+ sourceToken: z
437
+ .optional(Token)
438
+ .check(z.describe('Token and final amount paid out to the payee on close, if any.')),
439
+ })
440
+ .check(
441
+ z.describe(
442
+ 'A payment channel was closed, paying out the payee and refunding the payer.',
443
+ ),
444
+ ),
445
+ type: z
446
+ .literal('channel-closed')
447
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
448
+ })
449
+ .check(z.meta({ title: 'Channel closed' })),
450
+ z
451
+ .object({
452
+ ...base,
453
+ data: z
454
+ .object({
455
+ channelId: Schema.Hex.check(z.describe('Identifier of the payment channel.')),
456
+ payee: Schema.Address.check(
457
+ z.describe('Address that receives payments from the channel.'),
458
+ ),
459
+ payer: Schema.Address.check(z.describe('Address that funds the channel.')),
460
+ signer: Signer,
461
+ })
462
+ .check(z.describe('A pending payment-channel close request was cancelled.')),
463
+ type: z
464
+ .literal('channel-close-cancelled')
465
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
466
+ })
467
+ .check(z.meta({ title: 'Channel close cancelled' })),
468
+ z
469
+ .object({
470
+ ...base,
471
+ data: z
472
+ .object({
473
+ orderId: Schema.DecimalString.check(z.describe('Identifier of the DEX order.')),
474
+ side: z
475
+ .enum(['bid', 'ask'])
476
+ .check(z.describe('Order side: `bid` buys the base token, `ask` sells it.'), z.meta({ examples: ['bid'] })), // prettier-ignore
477
+ signer: Signer,
478
+ sourceToken: Token.check(z.describe('Token and amount escrowed for the order.')),
479
+ tick: z
480
+ .number()
481
+ .check(
482
+ z.int(),
483
+ z.describe('Price tick the order was placed at.'),
484
+ z.meta({ examples: [0] }),
485
+ ),
486
+ })
487
+ .check(z.describe('A limit order was placed on the stablecoin DEX.')),
488
+ type: z
489
+ .literal('order-placed')
490
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
491
+ })
492
+ .check(z.meta({ title: 'Order placed' })),
493
+ z
494
+ .object({
495
+ ...base,
496
+ data: z
497
+ .object({
498
+ orderId: Schema.DecimalString.check(
499
+ z.describe('Identifier of the cancelled DEX order.'),
500
+ ),
501
+ signer: Signer,
502
+ })
503
+ .check(z.describe('A DEX order was cancelled.')),
504
+ type: z
505
+ .literal('order-cancelled')
506
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
507
+ })
508
+ .check(z.meta({ title: 'Order cancelled' })),
509
+ z
510
+ .object({
511
+ ...base,
512
+ data: z
513
+ .object({
514
+ signer: Signer,
515
+ sourceToken: Token.check(z.describe('Token and amount of fees distributed.')),
516
+ validator: Schema.Address.check(
517
+ z.describe('Validator that received the distributed fees.'),
518
+ ),
519
+ })
520
+ .check(z.describe('Accrued validator fees were distributed.')),
521
+ type: z
522
+ .literal('fees-distributed')
523
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
524
+ })
525
+ .check(z.meta({ title: 'Fees distributed' })),
526
+ z
527
+ .object({
528
+ ...base,
529
+ data: z
530
+ .object({
531
+ destinationToken: Token.check(
532
+ z.describe('Token and amount received from the fee AMM.'),
533
+ ),
534
+ signer: Signer,
535
+ sourceToken: Token.check(z.describe('Token and amount sent into the fee AMM.')),
536
+ swapper: Schema.Address.check(z.describe('Address that performed the rebalance swap.')),
537
+ })
538
+ .check(z.describe('A fee-AMM rebalance swap between a validator and user token.')),
539
+ type: z
540
+ .literal('fee-rebalance-swap')
541
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
542
+ })
543
+ .check(z.meta({ title: 'Fees rebalanced' })),
544
+ z
545
+ .object({
546
+ ...base,
547
+ data: z
548
+ .object({
549
+ funder: Schema.Address.check(z.describe('Address that funded the reward pool.')),
550
+ signer: Signer,
551
+ sourceToken: Token.check(z.describe('Token and amount added to the reward pool.')),
552
+ })
553
+ .check(z.describe('A token reward pool was funded.')),
554
+ type: z
555
+ .literal('reward-distributed')
556
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
557
+ })
558
+ .check(z.meta({ title: 'Reward distributed' })),
559
+ z
560
+ .object({
561
+ ...base,
562
+ data: z
563
+ .object({
564
+ holder: Schema.Address.check(z.describe('Holder whose rewards were redirected.')),
565
+ recipient: Schema.Address.check(
566
+ z.describe('Address that now receives the holder’s rewards.'),
567
+ ),
568
+ signer: Signer,
569
+ })
570
+ .check(z.describe('A holder redirected where their token rewards are paid.')),
571
+ type: z
572
+ .literal('reward-recipient-set')
573
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
574
+ })
575
+ .check(z.meta({ title: 'Reward recipient set' })),
576
+ z
577
+ .object({
578
+ ...base,
579
+ data: z
580
+ .object({
581
+ account: Schema.Address.check(z.describe('Account the access key belongs to.')),
582
+ publicKey: Schema.Address.check(
583
+ z.describe('Public key of the access key whose limit changed.'),
584
+ ),
585
+ signer: Signer,
586
+ sourceToken: Token.check(
587
+ z.describe('Token and new spending limit set for the access key.'),
588
+ ),
589
+ })
590
+ .check(z.describe('An access key’s per-token spending limit was updated.')),
591
+ type: z
592
+ .literal('spending-limit-updated')
593
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
594
+ })
595
+ .check(z.meta({ title: 'Spending limit updated' })),
596
+ z
597
+ .object({
598
+ ...base,
599
+ data: z
600
+ .object({
601
+ amountFilled: Schema.DecimalString.check(
602
+ z.describe('Amount filled, in the token’s smallest unit.'),
603
+ ),
604
+ maker: Schema.Address.check(z.describe('Maker whose resting order was filled.')),
605
+ orderId: Schema.DecimalString.check(z.describe('Identifier of the filled DEX order.')),
606
+ partialFill: z
607
+ .boolean()
608
+ .check(
609
+ z.describe('Whether the order was only partially filled.'),
610
+ z.meta({ examples: [true] }),
611
+ ),
612
+ signer: Signer,
613
+ taker: Schema.Address.check(z.describe('Taker that filled the order.')),
614
+ })
615
+ .check(z.describe('A resting DEX order was filled by a taker.')),
616
+ type: z
617
+ .literal('order-filled')
618
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
619
+ })
620
+ .check(z.meta({ title: 'Order filled' })),
621
+ z
622
+ .object({
623
+ ...base,
624
+ data: z
625
+ .object({
626
+ flipTick: z
627
+ .number()
628
+ .check(
629
+ z.int(),
630
+ z.describe('Price tick the flipped order was placed at.'),
631
+ z.meta({ examples: [0] }),
632
+ ),
633
+ maker: Schema.Address.check(z.describe('Maker the order belongs to.')),
634
+ orderId: Schema.DecimalString.check(z.describe('Identifier of the DEX order.')),
635
+ side: z
636
+ .enum(['bid', 'ask'])
637
+ .check(z.describe('Side of the flipped order: `bid` buys the base token, `ask` sells it.'), z.meta({ examples: ['bid'] })), // prettier-ignore
638
+ signer: Signer,
639
+ sourceToken: Token.check(z.describe('Token and amount the flipped order rests with.')),
640
+ tick: z
641
+ .number()
642
+ .check(
643
+ z.int(),
644
+ z.describe('Price tick the original order filled at.'),
645
+ z.meta({ examples: [0] }),
646
+ ),
647
+ })
648
+ .check(z.describe('A flip order was rotated to the opposite side after filling.')),
649
+ type: z
650
+ .literal('order-flipped')
651
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
652
+ })
653
+ .check(z.meta({ title: 'Order flipped' })),
654
+ z
655
+ .object({
656
+ ...base,
657
+ data: z
658
+ .object({
659
+ base: Schema.Address.check(z.describe('Base token of the new trading pair.')),
660
+ key: Schema.Hex.check(z.describe('Identifier of the trading pair.')),
661
+ quote: Schema.Address.check(z.describe('Quote token of the new trading pair.')),
662
+ signer: Signer,
663
+ })
664
+ .check(z.describe('A new trading pair was created on the stablecoin DEX.')),
665
+ type: z
666
+ .literal('pair-created')
667
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
668
+ })
669
+ .check(z.meta({ title: 'Trading pair created' })),
670
+ z
671
+ .object({
672
+ ...base,
673
+ data: z
674
+ .object({
675
+ paused: z
676
+ .boolean()
677
+ .check(z.describe('Whether the token is now paused.'), z.meta({ examples: [true] })),
678
+ signer: Signer,
679
+ updater: Schema.Address.check(z.describe('Address that changed the pause state.')),
680
+ })
681
+ .check(z.describe('A TIP-20 token was paused or unpaused.')),
682
+ type: z
683
+ .literal('token-pause-set')
684
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
685
+ })
686
+ .check(z.meta({ title: 'Token pause set' })),
687
+ z
688
+ .object({
689
+ ...base,
690
+ data: z
691
+ .object({
692
+ signer: Signer,
693
+ supplyCap: Schema.DecimalString.check(
694
+ z.describe('New maximum total supply, in the token’s smallest unit.'),
695
+ ),
696
+ updater: Schema.Address.check(z.describe('Address that changed the supply cap.')),
697
+ })
698
+ .check(z.describe('A TIP-20 token’s supply cap was changed.')),
699
+ type: z
700
+ .literal('token-supply-cap-set')
701
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
702
+ })
703
+ .check(z.meta({ title: 'Token supply cap set' })),
704
+ z
705
+ .object({
706
+ ...base,
707
+ data: z
708
+ .object({
709
+ policyId: Schema.DecimalString.check(
710
+ z.describe('Identifier of the transfer policy now linked to the token.'),
711
+ ),
712
+ signer: Signer,
713
+ updater: Schema.Address.check(z.describe('Address that linked the transfer policy.')),
714
+ })
715
+ .check(z.describe('A TIP-20 token’s transfer policy was changed.')),
716
+ type: z
717
+ .literal('token-transfer-policy-set')
718
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
719
+ })
720
+ .check(z.meta({ title: 'Token transfer policy set' })),
721
+ z
722
+ .object({
723
+ ...base,
724
+ data: z
725
+ .object({
726
+ quoteToken: Schema.Address.check(z.describe('Token now used as the quote token.')),
727
+ signer: Signer,
728
+ updater: Schema.Address.check(z.describe('Address that changed the quote token.')),
729
+ })
730
+ .check(z.describe('A TIP-20 token’s quote token was changed.')),
731
+ type: z
732
+ .literal('token-quote-token-set')
733
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
734
+ })
735
+ .check(z.meta({ title: 'Token quote token set' })),
736
+ z
737
+ .object({
738
+ ...base,
739
+ data: z
740
+ .object({
741
+ nextQuoteToken: Schema.Address.check(
742
+ z.describe('Token queued to become the next quote token.'),
743
+ ),
744
+ signer: Signer,
745
+ updater: Schema.Address.check(z.describe('Address that queued the next quote token.')),
746
+ })
747
+ .check(z.describe('A TIP-20 token’s next quote token was queued.')),
748
+ type: z
749
+ .literal('token-next-quote-token-set')
750
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
751
+ })
752
+ .check(z.meta({ title: 'Token next quote token set' })),
753
+ z
754
+ .object({
755
+ ...base,
756
+ data: z
757
+ .object({
758
+ logoUri: z.string().check(
759
+ z.describe('New logo URL for the token.'),
760
+ z.meta({
761
+ examples: [
762
+ 'https://api.tempo.xyz/data/4217/icons/0x20c0000000000000000000008f5425160ebe5525.svg',
763
+ ],
764
+ }),
765
+ ),
766
+ signer: Signer,
767
+ updater: Schema.Address.check(z.describe('Address that changed the logo.')),
768
+ })
769
+ .check(z.describe('A TIP-20 token’s logo URI was changed.')),
770
+ type: z
771
+ .literal('token-logo-set')
772
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
773
+ })
774
+ .check(z.meta({ title: 'Token logo set' })),
775
+ z
776
+ .object({
777
+ ...base,
778
+ data: z
779
+ .object({
780
+ account: Schema.Address.check(z.describe('Account whose role membership changed.')),
781
+ granted: z
782
+ .boolean()
783
+ .check(
784
+ z.describe('Whether the role was granted (`true`) or revoked (`false`).'),
785
+ z.meta({ examples: [true] }),
786
+ ),
787
+ role: Schema.Hex.check(z.describe('Identifier of the role (a `bytes32` role hash).')),
788
+ sender: Schema.Address.check(z.describe('Address that made the role change.')),
789
+ signer: Signer,
790
+ })
791
+ .check(z.describe('A role was granted to or revoked from an account.')),
792
+ type: z
793
+ .literal('role-membership-set')
794
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
795
+ })
796
+ .check(z.meta({ title: 'Role membership set' })),
797
+ z
798
+ .object({
799
+ ...base,
800
+ data: z
801
+ .object({
802
+ newAdminRole: Schema.Hex.check(z.describe('Identifier of the new admin role.')),
803
+ role: Schema.Hex.check(z.describe('Identifier of the role whose admin changed.')),
804
+ sender: Schema.Address.check(z.describe('Address that changed the admin role.')),
805
+ signer: Signer,
806
+ })
807
+ .check(z.describe('A role’s admin role was changed.')),
808
+ type: z
809
+ .literal('role-admin-set')
810
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
811
+ })
812
+ .check(z.meta({ title: 'Role admin set' })),
813
+ z
814
+ .object({
815
+ ...base,
816
+ data: z
817
+ .object({
818
+ signer: Signer,
819
+ token: Schema.Address.check(z.describe('Token the user will pay fees in.')),
820
+ user: Schema.Address.check(z.describe('Address whose fee token was set.')),
821
+ })
822
+ .check(z.describe('An account’s fee-payment token was set.')),
823
+ type: z
824
+ .literal('fee-user-token-set')
825
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
826
+ })
827
+ .check(z.meta({ title: 'Fee user token set' })),
828
+ z
829
+ .object({
830
+ ...base,
831
+ data: z
832
+ .object({
833
+ signer: Signer,
834
+ token: Schema.Address.check(z.describe('Token the validator will be paid fees in.')),
835
+ validator: Schema.Address.check(z.describe('Validator whose payout token was set.')),
836
+ })
837
+ .check(z.describe('A validator’s fee-payout token was set.')),
838
+ type: z
839
+ .literal('fee-validator-token-set')
840
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
841
+ })
842
+ .check(z.meta({ title: 'Fee validator token set' })),
843
+ z
844
+ .object({
845
+ ...base,
846
+ data: z
847
+ .object({
848
+ policyId: Schema.DecimalString.check(z.describe('Identifier of the created policy.')),
849
+ policyType: z
850
+ .number()
851
+ .check(z.int(), z.describe('Numeric policy type.'), z.meta({ examples: [0] })),
852
+ signer: Signer,
853
+ updater: Schema.Address.check(z.describe('Address that created the policy.')),
854
+ })
855
+ .check(z.describe('A TIP-403 compliance policy was created.')),
856
+ type: z
857
+ .literal('policy-created')
858
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
859
+ })
860
+ .check(z.meta({ title: 'Policy created' })),
861
+ z
862
+ .object({
863
+ ...base,
864
+ data: z
865
+ .object({
866
+ admin: Schema.Address.check(z.describe('New admin of the policy.')),
867
+ policyId: Schema.DecimalString.check(z.describe('Identifier of the policy.')),
868
+ signer: Signer,
869
+ updater: Schema.Address.check(z.describe('Address that changed the policy admin.')),
870
+ })
871
+ .check(z.describe('A TIP-403 policy’s admin was changed.')),
872
+ type: z
873
+ .literal('policy-admin-set')
874
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
875
+ })
876
+ .check(z.meta({ title: 'Policy admin set' })),
877
+ z
878
+ .object({
879
+ ...base,
880
+ data: z
881
+ .object({
882
+ account: Schema.Address.check(z.describe('Account whose whitelist status changed.')),
883
+ allowed: z
884
+ .boolean()
885
+ .check(
886
+ z.describe('Whether the account is now whitelisted.'),
887
+ z.meta({ examples: [true] }),
888
+ ),
889
+ policyId: Schema.DecimalString.check(z.describe('Identifier of the policy.')),
890
+ signer: Signer,
891
+ updater: Schema.Address.check(z.describe('Address that changed the whitelist.')),
892
+ })
893
+ .check(z.describe('An account’s whitelist status on a policy was changed.')),
894
+ type: z
895
+ .literal('whitelist-updated')
896
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
897
+ })
898
+ .check(z.meta({ title: 'Whitelist updated' })),
899
+ z
900
+ .object({
901
+ ...base,
902
+ data: z
903
+ .object({
904
+ account: Schema.Address.check(z.describe('Account whose blacklist status changed.')),
905
+ policyId: Schema.DecimalString.check(z.describe('Identifier of the policy.')),
906
+ restricted: z
907
+ .boolean()
908
+ .check(
909
+ z.describe('Whether the account is now blacklisted.'),
910
+ z.meta({ examples: [true] }),
911
+ ),
912
+ signer: Signer,
913
+ updater: Schema.Address.check(z.describe('Address that changed the blacklist.')),
914
+ })
915
+ .check(z.describe('An account’s blacklist status on a policy was changed.')),
916
+ type: z
917
+ .literal('blacklist-updated')
918
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
919
+ })
920
+ .check(z.meta({ title: 'Blacklist updated' })),
921
+ z
922
+ .object({
923
+ ...base,
924
+ data: z
925
+ .object({
926
+ creator: Schema.Address.check(z.describe('Address that created the compound policy.')),
927
+ mintRecipientPolicyId: Schema.DecimalString.check(
928
+ z.describe('Sub-policy applied to mint recipients.'),
929
+ ),
930
+ policyId: Schema.DecimalString.check(
931
+ z.describe('Identifier of the created compound policy.'),
932
+ ),
933
+ recipientPolicyId: Schema.DecimalString.check(
934
+ z.describe('Sub-policy applied to recipients.'),
935
+ ),
936
+ senderPolicyId: Schema.DecimalString.check(
937
+ z.describe('Sub-policy applied to senders.'),
938
+ ),
939
+ signer: Signer,
940
+ })
941
+ .check(z.describe('A TIP-403 compound policy was created from sub-policies.')),
942
+ type: z
943
+ .literal('compound-policy-created')
944
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
945
+ })
946
+ .check(z.meta({ title: 'Compound policy created' })),
947
+ z
948
+ .object({
949
+ ...base,
950
+ data: z
951
+ .object({
952
+ masterAddress: Schema.Address.check(
953
+ z.describe('Address of the registered master contract.'),
954
+ ),
955
+ masterId: Schema.Hex.check(
956
+ z.describe('Identifier of the master contract (a `bytes4` selector).'),
957
+ ),
958
+ signer: Signer,
959
+ })
960
+ .check(z.describe('A master contract was registered in the address registry.')),
961
+ type: z
962
+ .literal('master-registered')
963
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
964
+ })
965
+ .check(z.meta({ title: 'Master registered' })),
966
+ z
967
+ .object({
968
+ ...base,
969
+ data: z
970
+ .object({
971
+ account: Schema.Address.check(z.describe('Account whose nonce advanced.')),
972
+ nonce: Schema.DecimalString.check(z.describe('New nonce value.')),
973
+ nonceKey: Schema.DecimalString.check(z.describe('Nonce key that was advanced.')),
974
+ signer: Signer,
975
+ })
976
+ .check(z.describe('An account or access-key nonce was advanced.')),
977
+ type: z
978
+ .literal('nonce-incremented')
979
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
980
+ })
981
+ .check(z.meta({ title: 'Nonce incremented' })),
982
+ z
983
+ .object({
984
+ ...base,
985
+ data: z
986
+ .object({
987
+ account: Schema.Address.check(z.describe('Account the witness belongs to.')),
988
+ signer: Signer,
989
+ witness: Schema.Hex.check(
990
+ z.describe('Witness commitment recorded for the key authorization.'),
991
+ ),
992
+ })
993
+ .check(z.describe('A key-authorization witness was recorded.')),
994
+ type: z
995
+ .literal('key-authorization-witness')
996
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
997
+ })
998
+ .check(z.meta({ title: 'Key authorization witnessed' })),
999
+ z
1000
+ .object({
1001
+ ...base,
1002
+ data: z
1003
+ .object({
1004
+ account: Schema.Address.check(z.describe('Account the witness belonged to.')),
1005
+ signer: Signer,
1006
+ witness: Schema.Hex.check(z.describe('Witness commitment that was consumed.')),
1007
+ })
1008
+ .check(z.describe('A key-authorization witness was consumed.')),
1009
+ type: z
1010
+ .literal('key-authorization-witness-burned')
1011
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1012
+ })
1013
+ .check(z.meta({ title: 'Key authorization witness burned' })),
1014
+ z
1015
+ .object({
1016
+ ...base,
1017
+ data: z
1018
+ .object({
1019
+ egress: z
1020
+ .string()
1021
+ .check(
1022
+ z.describe('Egress network endpoint.'),
1023
+ z.meta({ examples: ['https://validator.example.com:8443'] }),
1024
+ ),
1025
+ feeRecipient: Schema.Address.check(z.describe('Validator fee recipient address.')),
1026
+ index: Schema.DecimalString.check(z.describe('Validator index.')),
1027
+ ingress: z
1028
+ .string()
1029
+ .check(
1030
+ z.describe('Ingress network endpoint.'),
1031
+ z.meta({ examples: ['https://validator.example.com:8443'] }),
1032
+ ),
1033
+ publicKey: Schema.Hex.check(z.describe('Validator public key.')),
1034
+ signer: Signer,
1035
+ validator: Schema.Address.check(z.describe('Validator address.')),
1036
+ })
1037
+ .check(z.describe('A validator was added to the validator set.')),
1038
+ type: z
1039
+ .literal('validator-added')
1040
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1041
+ })
1042
+ .check(z.meta({ title: 'Validator added' })),
1043
+ z
1044
+ .object({
1045
+ ...base,
1046
+ data: z
1047
+ .object({
1048
+ index: Schema.DecimalString.check(z.describe('Validator index.')),
1049
+ signer: Signer,
1050
+ validator: Schema.Address.check(z.describe('Validator address.')),
1051
+ })
1052
+ .check(z.describe('A validator was deactivated.')),
1053
+ type: z
1054
+ .literal('validator-deactivated')
1055
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1056
+ })
1057
+ .check(z.meta({ title: 'Validator deactivated' })),
1058
+ z
1059
+ .object({
1060
+ ...base,
1061
+ data: z
1062
+ .object({
1063
+ caller: Schema.Address.check(z.describe('Address that rotated the validator.')),
1064
+ deactivatedIndex: Schema.DecimalString.check(
1065
+ z.describe('Index of the deactivated validator entry.'),
1066
+ ),
1067
+ egress: z
1068
+ .string()
1069
+ .check(
1070
+ z.describe('Egress network endpoint.'),
1071
+ z.meta({ examples: ['https://validator.example.com:8443'] }),
1072
+ ),
1073
+ index: Schema.DecimalString.check(z.describe('New validator index.')),
1074
+ ingress: z
1075
+ .string()
1076
+ .check(
1077
+ z.describe('Ingress network endpoint.'),
1078
+ z.meta({ examples: ['https://validator.example.com:8443'] }),
1079
+ ),
1080
+ newPublicKey: Schema.Hex.check(z.describe('New validator public key.')),
1081
+ oldPublicKey: Schema.Hex.check(z.describe('Previous validator public key.')),
1082
+ signer: Signer,
1083
+ validator: Schema.Address.check(z.describe('Validator address.')),
1084
+ })
1085
+ .check(z.describe('A validator’s keys and endpoints were rotated.')),
1086
+ type: z
1087
+ .literal('validator-rotated')
1088
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1089
+ })
1090
+ .check(z.meta({ title: 'Validator rotated' })),
1091
+ z
1092
+ .object({
1093
+ ...base,
1094
+ data: z
1095
+ .object({
1096
+ caller: Schema.Address.check(z.describe('Address that changed the fee recipient.')),
1097
+ feeRecipient: Schema.Address.check(z.describe('New validator fee recipient.')),
1098
+ index: Schema.DecimalString.check(z.describe('Validator index.')),
1099
+ signer: Signer,
1100
+ })
1101
+ .check(z.describe('A validator’s fee recipient was changed.')),
1102
+ type: z
1103
+ .literal('validator-fee-recipient-set')
1104
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1105
+ })
1106
+ .check(z.meta({ title: 'Validator fee recipient set' })),
1107
+ z
1108
+ .object({
1109
+ ...base,
1110
+ data: z
1111
+ .object({
1112
+ caller: Schema.Address.check(z.describe('Address that changed the endpoints.')),
1113
+ egress: z
1114
+ .string()
1115
+ .check(
1116
+ z.describe('Egress network endpoint.'),
1117
+ z.meta({ examples: ['https://validator.example.com:8443'] }),
1118
+ ),
1119
+ index: Schema.DecimalString.check(z.describe('Validator index.')),
1120
+ ingress: z
1121
+ .string()
1122
+ .check(
1123
+ z.describe('Ingress network endpoint.'),
1124
+ z.meta({ examples: ['https://validator.example.com:8443'] }),
1125
+ ),
1126
+ signer: Signer,
1127
+ })
1128
+ .check(z.describe('A validator’s network endpoints were changed.')),
1129
+ type: z
1130
+ .literal('validator-ip-set')
1131
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1132
+ })
1133
+ .check(z.meta({ title: 'Validator IP set' })),
1134
+ z
1135
+ .object({
1136
+ ...base,
1137
+ data: z
1138
+ .object({
1139
+ caller: Schema.Address.check(z.describe('Address that transferred ownership.')),
1140
+ index: Schema.DecimalString.check(z.describe('Validator index.')),
1141
+ newAddress: Schema.Address.check(z.describe('New validator owner address.')),
1142
+ oldAddress: Schema.Address.check(z.describe('Previous validator owner address.')),
1143
+ signer: Signer,
1144
+ })
1145
+ .check(z.describe('A validator’s owner address was changed.')),
1146
+ type: z
1147
+ .literal('validator-ownership-transferred')
1148
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1149
+ })
1150
+ .check(z.meta({ title: 'Validator ownership transferred' })),
1151
+ z
1152
+ .object({
1153
+ ...base,
1154
+ data: z
1155
+ .object({
1156
+ newOwner: Schema.Address.check(z.describe('New contract owner.')),
1157
+ oldOwner: Schema.Address.check(z.describe('Previous contract owner.')),
1158
+ signer: Signer,
1159
+ })
1160
+ .check(z.describe('Contract ownership was transferred.')),
1161
+ type: z
1162
+ .literal('ownership-transferred')
1163
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1164
+ })
1165
+ .check(z.meta({ title: 'Ownership transferred' })),
1166
+ z
1167
+ .object({
1168
+ ...base,
1169
+ data: z
1170
+ .object({
1171
+ index: Schema.DecimalString.check(z.describe('Validator index.')),
1172
+ publicKey: Schema.Hex.check(z.describe('Validator public key.')),
1173
+ signer: Signer,
1174
+ validator: Schema.Address.check(z.describe('Validator address.')),
1175
+ })
1176
+ .check(z.describe('A validator was migrated.')),
1177
+ type: z
1178
+ .literal('validator-migrated')
1179
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1180
+ })
1181
+ .check(z.meta({ title: 'Validator migrated' })),
1182
+ z
1183
+ .object({
1184
+ ...base,
1185
+ data: z
1186
+ .object({
1187
+ index: Schema.DecimalString.check(z.describe('Validator index.')),
1188
+ publicKey: Schema.Hex.check(z.describe('Validator public key.')),
1189
+ signer: Signer,
1190
+ validator: Schema.Address.check(z.describe('Validator address.')),
1191
+ })
1192
+ .check(z.describe('A validator migration was skipped.')),
1193
+ type: z
1194
+ .literal('validator-migration-skipped')
1195
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1196
+ })
1197
+ .check(z.meta({ title: 'Validator migration skipped' })),
1198
+ z
1199
+ .object({
1200
+ ...base,
1201
+ data: z
1202
+ .object({
1203
+ nextEpoch: Schema.DecimalString.check(
1204
+ z.describe('Epoch the rotation is scheduled for.'),
1205
+ ),
1206
+ previousEpoch: Schema.DecimalString.check(z.describe('Previous rotation epoch.')),
1207
+ signer: Signer,
1208
+ })
1209
+ .check(z.describe('A network-identity rotation epoch was scheduled.')),
1210
+ type: z
1211
+ .literal('network-identity-rotation-epoch-set')
1212
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1213
+ })
1214
+ .check(z.meta({ title: 'Network identity rotation epoch set' })),
1215
+ z
1216
+ .object({
1217
+ ...base,
1218
+ data: z
1219
+ .object({
1220
+ height: Schema.DecimalString.check(z.describe('Block height at initialization.')),
1221
+ signer: Signer,
1222
+ })
1223
+ .check(z.describe('A contract was initialized.')),
1224
+ type: z
1225
+ .literal('initialized')
1226
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1227
+ })
1228
+ .check(z.meta({ title: 'Initialized' })),
1229
+ z
1230
+ .object({
1231
+ ...base,
1232
+ data: z
1233
+ .object({ signer: Signer })
1234
+ .check(
1235
+ z.describe(
1236
+ 'A transaction the address sent or paid for that Tempo could not classify into a known activity type. The decoded and raw logs are surfaced in the top-level `events` array.',
1237
+ ),
1238
+ ),
1239
+ type: z
1240
+ .literal('unknown')
1241
+ .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
1242
+ })
1243
+ .check(z.meta({ title: 'Unknown' })),
1244
+ ] as const
1245
+
1246
+ /** A single classified activity item. */
1247
+ export const Item = z
1248
+ .union(items)
1249
+ .check(z.describe('One classified activity item in the address feed.'))
1250
+
1251
+ /** A folded access-key group: consecutive same-signer items on one UTC day. */
1252
+ export const Group = z
1253
+ .object({
1254
+ data: z
1255
+ .object({
1256
+ items: z.array(Item).check(z.describe('Activity items included in this group.')),
1257
+ signer: Schema.Address.check(
1258
+ z.describe('Access-key address that signed every item in the group.'),
1259
+ ),
1260
+ })
1261
+ .check(z.describe('Grouped activity entries folded together for readability.')),
1262
+ perspective: z
1263
+ .enum(['incoming', 'outgoing'])
1264
+ .check(
1265
+ z.describe(
1266
+ 'The shared perspective of the items in this group (see an item’s `perspective`).',
1267
+ ),
1268
+ ),
1269
+ id: z.string().check(
1270
+ z.describe('Stable group ID derived from the activity IDs inside the group.'),
1271
+ z.meta({
1272
+ examples: [
1273
+ '0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665-3..0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665-7:5',
1274
+ ],
1275
+ }),
1276
+ ),
1277
+ timestamp: z
1278
+ .iso.datetime()
1279
+ .check(z.describe('Timestamp of the first item in the group, as an ISO 8601 string.'), z.meta({ examples: ['2024-01-01T00:00:00Z'] })), // prettier-ignore
1280
+ title: z
1281
+ .literal('Group')
1282
+ .check(z.describe('Human-readable label for this entry, suitable for display in a feed.')),
1283
+ type: z.literal('group'),
1284
+ })
1285
+ .check(
1286
+ z.meta({ title: 'Group' }),
1287
+ z.describe('A group of consecutive activity items signed by the same access key.'),
1288
+ )
1289
+
1290
+ /** A single activity item, or a folded access-key group. */
1291
+ export const Entry = z
1292
+ .union([...items, Group])
1293
+ .check(z.describe('Either one activity item, or a grouped set of access-key activity items.'))
1294
+
1295
+ /** Schemas for the getAddressActivities operation. */
1296
+ export namespace getAddressActivities {
1297
+ /** Path parameters for address activity requests. */
1298
+ export const Params = z
1299
+ .object({
1300
+ address: Schema.Address.check(
1301
+ z.describe('Account address whose activity you want to list.'),
1302
+ ),
1303
+ })
1304
+ .check(z.describe('Path parameters for the address activity request.'))
1305
+
1306
+ /** Query parameters for address activity requests. */
1307
+ export const Query = z
1308
+ .strictObject({
1309
+ chainId: Schema.ChainIdQuery,
1310
+ cursor: Schema.Cursor,
1311
+ // Folding is opt-in: by default every item is returned at the root.
1312
+ group: z
1313
+ ._default(Schema.booleanQuery(), false)
1314
+ .check(
1315
+ z.describe(
1316
+ 'Set to `true` to fold consecutive items signed by the same access key on the same UTC day into `group` entries. Defaults to `false`, which returns every item separately.',
1317
+ ),
1318
+ z.meta({ examples: [true] }),
1319
+ ),
1320
+ limit: Schema.Limit,
1321
+ // Raw-log capture is opt-in: it can add a large `events` array to every
1322
+ // item, so the default feed stays lean.
1323
+ logs: z
1324
+ ._default(Schema.booleanQuery(), false)
1325
+ .check(
1326
+ z.describe(
1327
+ 'Set to `true` to attach each item’s full transaction log set (decoded when possible) as a top-level `events` array, so no co-located log is hidden. Defaults to `false`. `unknown` items always include it.',
1328
+ ),
1329
+ z.meta({ examples: [true] }),
1330
+ ),
1331
+ })
1332
+ .check(
1333
+ z.describe(
1334
+ 'Query parameters for the address activity request. This feed uses cursor pagination only because activity classification can fold a variable number of rows into each entry, so positional `page` pagination would not be stable.',
1335
+ ),
1336
+ )
1337
+
1338
+ /**
1339
+ * Page of classified activity. Mirrors the wallet activity feed: a
1340
+ * newest-first list of `data` entries (items, or access-key groups when
1341
+ * `group=true`) plus the root `nextCursor` pagination field shared with
1342
+ * other list endpoints.
1343
+ */
1344
+ export const Response = z
1345
+ .object({
1346
+ data: z.array(Entry).check(z.describe('Activity feed entries for the address.')),
1347
+ nextCursor: Schema.NextCursor,
1348
+ })
1349
+ .check(z.describe('A page of human-readable activity for the address.'))
1350
+ }
1351
+
1352
+ /** Schemas for the getTransactionActivities operation. */
1353
+ export namespace getTransactionActivities {
1354
+ /** Path parameters for transaction activity requests. */
1355
+ export const Params = z
1356
+ .object({
1357
+ transactionHash: Schema.hash(exampleHash).check(
1358
+ z.describe(
1359
+ 'A 32-byte hash, `0x`-prefixed and returned in lowercase, that identifies the transaction whose activity you want to list.',
1360
+ ),
1361
+ ),
1362
+ })
1363
+ .check(z.describe('Path parameters for the transaction activity request.'))
1364
+
1365
+ /** Query parameters for transaction activity requests. */
1366
+ export const Query = z
1367
+ .strictObject({
1368
+ chainId: Schema.ChainIdQuery,
1369
+ // Raw-log capture is opt-in: it can add a large `events` array to every
1370
+ // item, so the default response stays lean.
1371
+ logs: z
1372
+ ._default(Schema.booleanQuery(), false)
1373
+ .check(
1374
+ z.describe(
1375
+ 'Set to `true` to attach each item’s full transaction log set (decoded when possible) as a top-level `events` array, so no co-located log is hidden. Defaults to `false`. `unknown` items always include it.',
1376
+ ),
1377
+ z.meta({ examples: [true] }),
1378
+ ),
1379
+ })
1380
+ .check(z.describe('Query parameters for the transaction activity request.'))
1381
+
1382
+ /**
1383
+ * The classified activity for one transaction: a list of `data` items
1384
+ * (usually one headline item, occasionally more) describing what the
1385
+ * transaction did onchain. Unpaginated — a single transaction is finite —
1386
+ * so there is no `nextCursor`.
1387
+ */
1388
+ export const Response = z
1389
+ .object({
1390
+ data: z.array(Item).check(z.describe('Activity items classified from the transaction.')),
1391
+ })
1392
+ .check(z.describe('The human-readable activity that happened on a transaction.'))
1393
+ }
1394
+ }
1395
+
1396
+ /** A token and amount in the public resource shape (derived from {@link schema.Token}). */
1397
+ export type Token = z.output<typeof schema.Token>
1398
+
1399
+ /** A single classified activity item (derived from {@link schema.Item}). */
1400
+ export type Item = z.output<typeof schema.Item>
1401
+
1402
+ /** A single activity item, or a folded access-key group (derived from {@link schema.Entry}). */
1403
+ export type Entry = z.output<typeof schema.Entry>
1404
+
1405
+ /**
1406
+ * A classified item before {@link list} stamps its display {@link Item.title}.
1407
+ * Internal classifiers build these; `list` derives `title` from `type` via
1408
+ * {@link titleByType}, so each branch never repeats the human label.
1409
+ */
1410
+ type RawItem = {
1411
+ [type in Item['type']]: Omit<Extract<Item, { type: type }>, 'title'>
1412
+ }[Item['type']]
1413
+
1414
+ /**
1415
+ * Human-readable label for each activity {@link Item.type}, mirroring the per-variant
1416
+ * OpenAPI schema titles. Used to stamp {@link Item.title} during {@link list}.
1417
+ */
1418
+ const titleByType: Record<Item['type'], string> = {
1419
+ approval: 'Allowance approved',
1420
+ 'access-key-created': 'Access key created',
1421
+ 'access-key-revoked': 'Access key revoked',
1422
+ 'blacklist-updated': 'Blacklist updated',
1423
+ burn: 'Token burned',
1424
+ 'burn-blocked': 'Burn blocked',
1425
+ 'channel-close-cancelled': 'Channel close cancelled',
1426
+ 'channel-closed': 'Channel closed',
1427
+ 'channel-funded': 'Channel funded',
1428
+ 'channel-opened': 'Channel opened',
1429
+ 'channel-settled': 'Channel settled',
1430
+ 'compound-policy-created': 'Compound policy created',
1431
+ 'fee-rebalance-swap': 'Fees rebalanced',
1432
+ 'fee-user-token-set': 'Fee user token set',
1433
+ 'fee-validator-token-set': 'Fee validator token set',
1434
+ 'fees-distributed': 'Fees distributed',
1435
+ initialized: 'Initialized',
1436
+ 'key-authorization-witness': 'Key authorization witnessed',
1437
+ 'key-authorization-witness-burned': 'Key authorization witness burned',
1438
+ 'master-registered': 'Master registered',
1439
+ mint: 'Token minted',
1440
+ 'network-identity-rotation-epoch-set': 'Network identity rotation epoch set',
1441
+ 'nonce-incremented': 'Nonce incremented',
1442
+ 'order-cancelled': 'Order cancelled',
1443
+ 'order-filled': 'Order filled',
1444
+ 'order-flipped': 'Order flipped',
1445
+ 'order-placed': 'Order placed',
1446
+ 'ownership-transferred': 'Ownership transferred',
1447
+ 'pair-created': 'Trading pair created',
1448
+ 'policy-admin-set': 'Policy admin set',
1449
+ 'policy-created': 'Policy created',
1450
+ 'reward-distributed': 'Reward distributed',
1451
+ 'reward-recipient-set': 'Reward recipient set',
1452
+ 'role-admin-set': 'Role admin set',
1453
+ 'role-membership-set': 'Role membership set',
1454
+ 'session-closed': 'Session closed',
1455
+ 'spending-limit-updated': 'Spending limit updated',
1456
+ swap: 'Tokens swapped',
1457
+ 'token-created': 'Token created',
1458
+ 'token-logo-set': 'Token logo set',
1459
+ 'token-next-quote-token-set': 'Token next quote token set',
1460
+ 'token-pause-set': 'Token pause set',
1461
+ 'token-quote-token-set': 'Token quote token set',
1462
+ 'token-supply-cap-set': 'Token supply cap set',
1463
+ 'token-transfer-policy-set': 'Token transfer policy set',
1464
+ transfer: 'Token transferred',
1465
+ unknown: 'Unknown',
1466
+ 'validator-added': 'Validator added',
1467
+ 'validator-deactivated': 'Validator deactivated',
1468
+ 'validator-fee-recipient-set': 'Validator fee recipient set',
1469
+ 'validator-ip-set': 'Validator IP set',
1470
+ 'validator-migrated': 'Validator migrated',
1471
+ 'validator-migration-skipped': 'Validator migration skipped',
1472
+ 'validator-ownership-transferred': 'Validator ownership transferred',
1473
+ 'validator-rotated': 'Validator rotated',
1474
+ 'whitelist-updated': 'Whitelist updated',
1475
+ }
1476
+
1477
+ /** Stamps the display {@link Item.title} onto a classified item from {@link titleByType}. */
1478
+ function withTitle<item extends RawItem>(item: item): item & { title: string } {
1479
+ return { ...item, title: titleByType[item.type] }
1480
+ }
1481
+
1482
+ /**
1483
+ * Human-readable label for each `viem/tempo` ABI event name, used to title the
1484
+ * entries of an item's raw `events` capture. Keyed by event name (not activity
1485
+ * type), so co-located logs read naturally even when they are not the headline.
1486
+ */
1487
+ const titleByEventName: Record<string, string> = {
1488
+ AccessKeySpend: 'Access key spent',
1489
+ Approval: 'Allowance approved',
1490
+ BlacklistUpdated: 'Blacklist updated',
1491
+ Burn: 'Token burned',
1492
+ BurnBlocked: 'Burn blocked',
1493
+ ChannelClosed: 'Channel closed',
1494
+ ChannelOpened: 'Channel opened',
1495
+ CloseRequestCancelled: 'Channel close cancelled',
1496
+ CloseRequested: 'Channel close requested',
1497
+ CompoundPolicyCreated: 'Compound policy created',
1498
+ FeeRecipientUpdated: 'Validator fee recipient set',
1499
+ FeesDistributed: 'Fees distributed',
1500
+ Initialized: 'Initialized',
1501
+ IpAddressesUpdated: 'Validator IP set',
1502
+ KeyAuthorizationWitness: 'Key authorization witnessed',
1503
+ KeyAuthorizationWitnessBurned: 'Key authorization witness burned',
1504
+ KeyAuthorized: 'Access key created',
1505
+ KeyRevoked: 'Access key revoked',
1506
+ LogoURIUpdated: 'Token logo set',
1507
+ MasterRegistered: 'Master registered',
1508
+ Mint: 'Token minted',
1509
+ NetworkIdentityRotationEpochSet: 'Network identity rotation epoch set',
1510
+ NextQuoteTokenSet: 'Token next quote token set',
1511
+ NonceIncremented: 'Nonce incremented',
1512
+ OrderCancelled: 'Order cancelled',
1513
+ OrderFilled: 'Order filled',
1514
+ OrderFlipped: 'Order flipped',
1515
+ OrderPlaced: 'Order placed',
1516
+ OwnershipTransferred: 'Ownership transferred',
1517
+ PairCreated: 'Trading pair created',
1518
+ PauseStateUpdate: 'Token pause set',
1519
+ PolicyAdminUpdated: 'Policy admin set',
1520
+ PolicyCreated: 'Policy created',
1521
+ QuoteTokenUpdate: 'Token quote token set',
1522
+ RebalanceSwap: 'Fees rebalanced',
1523
+ RewardDistributed: 'Reward distributed',
1524
+ RewardRecipientSet: 'Reward recipient set',
1525
+ RoleAdminUpdated: 'Role admin set',
1526
+ RoleMembershipUpdated: 'Role membership set',
1527
+ Settled: 'Channel settled',
1528
+ SkippedValidatorMigration: 'Validator migration skipped',
1529
+ SpendingLimitUpdated: 'Spending limit updated',
1530
+ SupplyCapUpdate: 'Token supply cap set',
1531
+ TokenCreated: 'Token created',
1532
+ TopUp: 'Channel funded',
1533
+ Transfer: 'Token transferred',
1534
+ TransferPolicyUpdate: 'Token transfer policy set',
1535
+ TransferWithMemo: 'Token transferred',
1536
+ UserTokenSet: 'Fee user token set',
1537
+ ValidatorAdded: 'Validator added',
1538
+ ValidatorDeactivated: 'Validator deactivated',
1539
+ ValidatorMigrated: 'Validator migrated',
1540
+ ValidatorOwnershipTransferred: 'Validator ownership transferred',
1541
+ ValidatorRotated: 'Validator rotated',
1542
+ ValidatorTokenSet: 'Fee validator token set',
1543
+ WhitelistUpdated: 'Whitelist updated',
1544
+ }
1545
+
1546
+ // keccak256("CloseRequested(bytes32,address,address,uint256)") — emitted by a
1547
+ // payment channel when settlement is requested; matched on the raw selector
1548
+ // because it is not part of the merged `viem/tempo` ABI.
1549
+ const closeRequestedSelector = '0xf5a36fc00a96cbb9cf1f8f59299165e1d8ffffe94396d82904b4da524d16bbce'
1550
+
1551
+ // keccak256("AccessKeySpend(address,address,address,uint256,uint256)") — emitted
1552
+ // by the accountKeychain whenever a tx is signed by an access key. topic2 is the
1553
+ // access-key publicKey, i.e. the real signer.
1554
+ const accessKeySpendSelector = '0xe0815e3aaadddf4dd75bde97fc060f0c38afe18e87a169be86a3f5c28247f192'
1555
+
1556
+ /** TIP-20 / ERC-20 `Transfer` event signature for the decoded CTE. */
1557
+ const transferSignature = 'event Transfer(address indexed from, address indexed to, uint256 value)'
1558
+
1559
+ // TIP-20 payment-channel reserve precompile. Not exported by `viem/tempo`
1560
+ // `Addresses`, so the known address is pinned here: channel events
1561
+ // (`ChannelOpened`/`Settled`/`TopUp`/`ChannelClosed`) are emitted here, and
1562
+ // their paired TIP-20 `Transfer` legs use it as the funding counterparty, which
1563
+ // is how `channel-funded`/`channel-settled`/`channel-closed` recover the token.
1564
+ const tip20ChannelReserve = '0x4d50500000000000000000000000000000000000' as Address.Address
1565
+
1566
+ /** Number of grouped entries to target before stopping the page-fetch loop. */
1567
+ const minGroupEntries = 5
1568
+
1569
+ /** Largest number of items folded into a single access-key group. */
1570
+ const maxGroupSize = 50
1571
+
1572
+ /** Token-bearing keys on an item's `data` payload, walked for enrichment. */
1573
+ const tokenKeys = ['destinationToken', 'refund', 'sourceToken'] as const
1574
+
1575
+ /** The base fields ({@link schema.base}) every activity item carries. */
1576
+ type Base = {
1577
+ id: string
1578
+ logIndex: number
1579
+ perspective: 'incoming' | 'outgoing'
1580
+ timestamp: string
1581
+ transactionHash: Hex.Hex
1582
+ }
1583
+
1584
+ /** Builds common public activity fields from the normalized transaction hash and representative log. */
1585
+ function baseFor(options: {
1586
+ logIndex: number
1587
+ perspective: 'incoming' | 'outgoing'
1588
+ timestamp: string
1589
+ transactionHash: Hex.Hex
1590
+ }): Base {
1591
+ return {
1592
+ perspective: options.perspective,
1593
+ id: `${options.transactionHash}-${options.logIndex}`,
1594
+ logIndex: options.logIndex,
1595
+ timestamp: options.timestamp,
1596
+ transactionHash: options.transactionHash,
1597
+ }
1598
+ }
1599
+
1600
+ /**
1601
+ * A normalized provider row. Mirrors the TIDX `receipts ⋈ logs` join
1602
+ * (`source: 'q1'`, full log columns) and the decoded `Transfer` projection
1603
+ * (`source: 'q2'`, transfer columns), so the classification pipeline does not
1604
+ * care which query produced the row.
1605
+ */
1606
+ export type Row = {
1607
+ blockNum: number
1608
+ blockTimestamp: number
1609
+ data: string | null
1610
+ feePayer: Address.Address | null
1611
+ logAddress: Address.Address
1612
+ logIdx: number
1613
+ selector: string | null
1614
+ source: 'q1' | 'q2'
1615
+ topic1: string | null
1616
+ topic2: string | null
1617
+ topic3: string | null
1618
+ transferAmount?: bigint
1619
+ transferFrom?: Address.Address
1620
+ transferTo?: Address.Address
1621
+ transferToken?: Address.Address
1622
+ txHash: Hex.Hex
1623
+ txIdx: number
1624
+ txSender: Address.Address | null
1625
+ }
1626
+
1627
+ /** A swappable activity data source. */
1628
+ export type Provider = {
1629
+ /** Encodes the opaque cursor that resumes strictly after `group`. */
1630
+ cursorForGroup(group: { blockNum: number; txIdx: number }): string
1631
+ /** Fetches a window of merged rows in `(blockNum desc, txIdx desc)` order. */
1632
+ fetchRows(options: {
1633
+ address: Address.Address
1634
+ cursor?: string | undefined
1635
+ limit: number
1636
+ }): Promise<{ rows: Row[] }>
1637
+ }
1638
+
1639
+ /** Internal group of rows that share a transaction hash. */
1640
+ type TxGroup = {
1641
+ blockNum: number
1642
+ blockTimestamp: number
1643
+ feePayer: Address.Address | null
1644
+ rows: Row[]
1645
+ txHash: Hex.Hex
1646
+ txIdx: number
1647
+ txSender: Address.Address | null
1648
+ }
1649
+
1650
+ /**
1651
+ * Creates address-scoped activity handlers, mounted under the `/addresses`
1652
+ * composer. Exposes `GET /:address/activities`.
1653
+ *
1654
+ * Runs the full activity classification pipeline: three TIDX sub-queries
1655
+ * (outbound `receipts ⋈ logs`, inbound transfers, outbound transfers) merged
1656
+ * and decoded into typed items (`transfer`/`mint`/`burn`/`swap`/
1657
+ * `approval`/`session-closed`/`access-key-created`/`access-key-revoked`/
1658
+ * `token-created`/`burn-blocked`/`channel-opened`/`channel-funded`/
1659
+ * `channel-settled`/`channel-closed`/`channel-close-cancelled`/
1660
+ * `order-placed`/`order-cancelled`/`order-filled`/`order-flipped`/
1661
+ * `pair-created`/`fees-distributed`/`fee-rebalance-swap`/`reward-distributed`/
1662
+ * `reward-recipient-set`/`spending-limit-updated`/ token, fee, policy, registry,
1663
+ * access-key-witness and validator admin events (e.g. `token-pause-set`,
1664
+ * `policy-created`, `validator-added`)/`unknown`), then folds
1665
+ * consecutive access-key-signed items into `group` entries. Pagination uses the
1666
+ * same `(block_num, tx_idx)` keyset cursor the pipeline emits.
1667
+ */
1668
+ export function addresses() {
1669
+ return new Hono<App.Environment>().get(
1670
+ '/:address/activities',
1671
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
1672
+ OpenApi.validate('param', schema.getAddressActivities.Params, {
1673
+ code: 'address_invalid',
1674
+ message: 'Check the account address and try again.',
1675
+ }),
1676
+ OpenApi.validate('query', schema.getAddressActivities.Query, {
1677
+ code: 'query_invalid',
1678
+ message: 'Check the query parameters and try again.',
1679
+ }),
1680
+ OpenApi.describeRoute({
1681
+ description:
1682
+ 'Get a human-readable feed of what an address has been doing onchain, including payments sent and received, swaps, approvals, and more.',
1683
+ operationId: 'getAddressActivities',
1684
+ responses: OpenApi.responses({
1685
+ errors: { 502: 'Tempo could not read activity data from the upstream indexer.' },
1686
+ success: {
1687
+ description: 'A page of address activity entries.',
1688
+ schema: schema.getAddressActivities.Response,
1689
+ },
1690
+ }),
1691
+ summary: 'List address activities',
1692
+ tags: ['Activities'],
1693
+ }),
1694
+ Cache.response<App.Environment>({
1695
+ cacheControl: Cache.policies.feed,
1696
+ name: 'tempo-api:addresses:v1',
1697
+ key: (c) => Cache.urlKey(c, schema.getAddressActivities.Query),
1698
+ }),
1699
+ async (c) => {
1700
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
1701
+ if (OpenApi.narrowValidation)
1702
+ return OpenApi.validationError(c, {
1703
+ code: 'address_invalid',
1704
+ message: 'Check the account address and try again.',
1705
+ })
1706
+ if (OpenApi.narrowValidation)
1707
+ return OpenApi.validationError(c, {
1708
+ code: 'query_invalid',
1709
+ message: 'Check the query parameters and try again.',
1710
+ })
1711
+
1712
+ const { address } = c.req.valid('param')
1713
+ const query = c.req.valid('query')
1714
+
1715
+ // Cursor pages are anchored below the head and effectively immutable, so
1716
+ // cache them aggressively; the head page keeps the route's short `feed`
1717
+ // default.
1718
+ if (query.cursor !== undefined) Cache.setPolicy(c, Cache.policies.immutable)
1719
+
1720
+ try {
1721
+ const chainId = query.chainId ?? c.get('chainId')
1722
+ const provider = tidx({ chainId, tidx: c.get('getTidx')(chainId) })
1723
+ const { items, nextCursor } = await Timing.time(c, 'address_activities', () =>
1724
+ Store.memoize(
1725
+ () =>
1726
+ list(provider, {
1727
+ address: address as Hex.Hex,
1728
+ cursor: query.cursor,
1729
+ group: query.group,
1730
+ limit: query.limit,
1731
+ logs: query.logs,
1732
+ store: c.get('store'),
1733
+ }),
1734
+ {
1735
+ key: `activities:v1:${chainId}:${address.toLowerCase()}:${query.group ?? false}:${query.logs ?? false}:${query.cursor ?? 'head'}:${query.limit}`,
1736
+ store: c.get('store'),
1737
+ ttl: Ttl.seconds(15),
1738
+ },
1739
+ ),
1740
+ )
1741
+
1742
+ // Resolve token metadata for every token referenced across the page and
1743
+ // spread it into each item's stub tokens. Goes through the shared
1744
+ // `Tokens.getTokenMetadata` path so it reuses the `token:v1:…` cache with
1745
+ // `/tokens` and `/transfers` instead of a parallel metadata fetch.
1746
+ await enrichTokens(c, { chainId, items })
1747
+
1748
+ // Fold consecutive access-key-signed items into `group` entries only
1749
+ // when requested; otherwise every item is returned at the root.
1750
+ const data = query.group ? groupByAccessKey(items) : items
1751
+
1752
+ return c.json(
1753
+ Response.validated(schema.getAddressActivities.Response, {
1754
+ data,
1755
+ nextCursor,
1756
+ }),
1757
+ 200,
1758
+ )
1759
+ } catch (cause) {
1760
+ return Response.upstream(c, cause)
1761
+ }
1762
+ },
1763
+ )
1764
+ }
1765
+
1766
+ /**
1767
+ * Creates transaction-scoped activity handlers, mounted under the
1768
+ * `/transactions` composer. Exposes `GET /:transactionHash/activities`.
1769
+ *
1770
+ * Runs the same `groupByTx` → `classifyGroup` pipeline the address feed uses
1771
+ * over a single transaction's logs, anchored on the transaction sender as the
1772
+ * viewer (a transaction-scoped feed has no subject account). Returns the
1773
+ * classified item(s) — usually one headline activity — with the full per-tx
1774
+ * event set available via `logs=true`.
1775
+ */
1776
+ export function transactions() {
1777
+ return new Hono<App.Environment>().get(
1778
+ '/:transactionHash/activities',
1779
+ Auth.policy({ apiKey: { scopes: ['data:read'] }, mpp: true, public: true }),
1780
+ OpenApi.validate('param', schema.getTransactionActivities.Params, {
1781
+ code: 'transaction_invalid',
1782
+ message: 'Check the transaction hash and try again.',
1783
+ }),
1784
+ OpenApi.validate('query', schema.getTransactionActivities.Query, {
1785
+ code: 'query_invalid',
1786
+ message: 'Check the query parameters and try again.',
1787
+ }),
1788
+ OpenApi.describeRoute({
1789
+ description:
1790
+ 'Get a human-readable list of what happened on one transaction onchain, including payments, swaps, approvals, and more.',
1791
+ operationId: 'getTransactionActivities',
1792
+ responses: OpenApi.responses({
1793
+ errors: {
1794
+ 404: {
1795
+ description: 'No mined transaction was found for that hash.',
1796
+ codes: ['transaction_not_found'],
1797
+ },
1798
+ 502: 'Tempo could not read activity data from the upstream indexer.',
1799
+ },
1800
+ success: {
1801
+ description: 'The classified activity for the transaction.',
1802
+ schema: schema.getTransactionActivities.Response,
1803
+ },
1804
+ }),
1805
+ summary: 'List transaction activities',
1806
+ tags: ['Activities'],
1807
+ }),
1808
+ Cache.response<App.Environment>({
1809
+ // Activity exists only once the transaction is mined; once mined it is
1810
+ // immutable. Default to no-store so a 404 (unknown / not-yet-mined hash)
1811
+ // is not cached with a long header that would mask the eventual result;
1812
+ // the handler upgrades the 200 to `immutable` below.
1813
+ cacheControl: Cache.policies.noStore,
1814
+ name: 'tempo-api:transactions:v1',
1815
+ key: (c) => Cache.urlKey(c, schema.getTransactionActivities.Query),
1816
+ }),
1817
+ async (c) => {
1818
+ if (Auth.narrowAccess) return Auth.paidAccessError(c)
1819
+ if (OpenApi.narrowValidation)
1820
+ return OpenApi.validationError(c, {
1821
+ code: 'transaction_invalid',
1822
+ message: 'Check the transaction hash and try again.',
1823
+ })
1824
+ if (OpenApi.narrowValidation)
1825
+ return OpenApi.validationError(c, {
1826
+ code: 'query_invalid',
1827
+ message: 'Check the query parameters and try again.',
1828
+ })
1829
+
1830
+ const { transactionHash } = c.req.valid('param')
1831
+ const query = c.req.valid('query')
1832
+ const chainId = query.chainId ?? c.get('chainId')
1833
+
1834
+ try {
1835
+ const items = await Timing.time(c, 'transaction_activities', () =>
1836
+ Store.memoize(
1837
+ () =>
1838
+ getTransactionActivities({
1839
+ chainId,
1840
+ logs: query.logs,
1841
+ store: c.get('store'),
1842
+ tidx: c.get('getTidx')(chainId),
1843
+ transactionHash,
1844
+ }),
1845
+ {
1846
+ key: `activities:v1:${chainId}:tx:${transactionHash.toLowerCase()}:${query.logs ?? false}`,
1847
+ store: c.get('store'),
1848
+ ttl: Ttl.seconds(15),
1849
+ },
1850
+ ),
1851
+ )
1852
+
1853
+ if (items === null)
1854
+ return Response.error(c, {
1855
+ code: 'transaction_not_found',
1856
+ message: 'Transaction not found',
1857
+ status: 404,
1858
+ })
1859
+
1860
+ // A mined transaction's activity is final, so cache it aggressively.
1861
+ Cache.setPolicy(c, Cache.policies.immutable)
1862
+
1863
+ // Resolve token metadata for every token referenced and spread it into
1864
+ // each item's stub tokens (shared `token:v1:…` cache path).
1865
+ await enrichTokens(c, { chainId, items })
1866
+
1867
+ return c.json(
1868
+ Response.validated(schema.getTransactionActivities.Response, { data: items }),
1869
+ 200,
1870
+ )
1871
+ } catch (cause) {
1872
+ return Response.upstream(c, cause)
1873
+ }
1874
+ },
1875
+ )
1876
+ }
1877
+
1878
+ /**
1879
+ * Classifies the activity for a single transaction. Fetches the transaction's
1880
+ * receipt (for its block position, sender, and fee payer) and its logs, then
1881
+ * runs the same `groupByTx` → `classifyGroup` pipeline the address feed uses,
1882
+ * anchored on the transaction sender as the viewer. Returns `null` when no
1883
+ * receipt exists for the hash (an unknown or not-yet-mined transaction), or an
1884
+ * empty list for a mined transaction that emitted no classifiable logs.
1885
+ *
1886
+ * Items carry stub tokens; the caller enriches them via {@link enrichTokens}.
1887
+ */
1888
+ export async function getTransactionActivities(
1889
+ options: getTransactionActivities.Options,
1890
+ ): Promise<Item[] | null> {
1891
+ const { chainId, logs = false, tidx: tidxClient } = options
1892
+ const hash = options.transactionHash.toLowerCase()
1893
+
1894
+ // The `receipts` table is indexed by `tx_hash`, so this point lookup needs no
1895
+ // block bound; it also yields the block number the `logs` lookup requires.
1896
+ // Raw-table query (no signature-decoded CTE), so the inline SQL is cast to
1897
+ // `string`.
1898
+ const receiptResult = await tidxClient.fetch({
1899
+ chainId,
1900
+ query:
1901
+ `SELECT block_num, tx_idx, "from", fee_payer FROM receipts WHERE tx_hash = '${hash}' LIMIT 1` as string,
1902
+ })
1903
+ const receipt = receiptResult.rows[0]
1904
+ if (!receipt) return null
1905
+
1906
+ const blockNum = Number(Value.toNumber(receipt['block_num']))
1907
+ const feePayer = lowerOrNull(receipt['fee_payer'])
1908
+ const txSender = lowerOrNull(receipt['from'])
1909
+
1910
+ // `logs` is sorted by block position, so a bare `tx_hash =` scans to genesis;
1911
+ // pair it with the receipt's block number (see `embedReceipts` in receipts.ts).
1912
+ const logsResult = await tidxClient.fetch({
1913
+ chainId,
1914
+ query:
1915
+ `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` as string,
1916
+ })
1917
+
1918
+ const rows: Row[] = logsResult.rows.map((r) => ({
1919
+ blockNum: Number(Value.toNumber(r['block_num'])),
1920
+ blockTimestamp: Number(Value.toNumber(r['block_timestamp'])),
1921
+ data: Value.toText(r['data']) ?? null,
1922
+ feePayer,
1923
+ logAddress: lower(r['address']),
1924
+ logIdx: Number(Value.toNumber(r['log_idx'])),
1925
+ selector: Value.toText(r['selector']) ?? null,
1926
+ source: 'q1' as const,
1927
+ topic1: Value.toText(r['topic1']) ?? null,
1928
+ topic2: Value.toText(r['topic2']) ?? null,
1929
+ topic3: Value.toText(r['topic3']) ?? null,
1930
+ txHash: lowerHash(r['tx_hash']),
1931
+ txIdx: Number(Value.toNumber(r['tx_idx'])),
1932
+ txSender,
1933
+ }))
1934
+
1935
+ // A mined transaction with no logs (e.g. a plain value transfer) has no
1936
+ // classifiable activity; return an empty list rather than a fabricated item.
1937
+ if (rows.length === 0) return []
1938
+
1939
+ // Best-effort MPP service directory: a fetch failure yields an empty map, so
1940
+ // enrichment never blocks classification.
1941
+ const fingerprintMap = await Mpp.fingerprintMap({ store: options.store }).catch(() => ({}))
1942
+
1943
+ // Anchor the viewer on the transaction sender: the sender initiated every
1944
+ // activity in the transaction, so `perspective` is `outgoing` and `signer`
1945
+ // resolves to `self`. Fall back to the zero address when the sender is absent.
1946
+ const viewer = (txSender ?? zeroAddress) as Address.Address
1947
+ const group = groupByTx(rows)[0]!
1948
+ return classifyGroup(group, viewer, fingerprintMap, logs).map(withTitle)
1949
+ }
1950
+
1951
+ export declare namespace getTransactionActivities {
1952
+ /** Options for {@link getTransactionActivities}. */
1953
+ type Options = {
1954
+ /** Source chain id. */
1955
+ chainId: number
1956
+ /** Attach each item's full transaction `events` capture (decoded + raw logs). */
1957
+ logs?: boolean | undefined
1958
+ /** Cache store for the MPP fingerprint-directory lookup. */
1959
+ store: Store.Store
1960
+ /** TIDX query client. */
1961
+ tidx: Tidx.Client
1962
+ /** Transaction hash to classify. */
1963
+ transactionHash: string
1964
+ }
1965
+ }
1966
+
1967
+ /**
1968
+ * Lists classified activity for an address by running the shared pipeline
1969
+ * (`groupByTx` → `classifyGroup` → `enrichItem`) over rows from a
1970
+ * {@link Provider}. Returns enriched, chain-tagged items plus the next cursor;
1971
+ * callers run {@link groupByAccessKey} over the result.
1972
+ */
1973
+ export async function list(provider: Provider, options: list.Options): Promise<list.ReturnType> {
1974
+ const { address, cursor, group = false, limit = 20, logs = false } = options
1975
+ const viewer = address.toLowerCase() as Address.Address
1976
+
1977
+ // Best-effort MPP service directory: a fetch failure yields an empty map, so
1978
+ // enrichment never blocks the feed.
1979
+ const fingerprintMap = await Mpp.fingerprintMap({ store: options.store }).catch(() => ({}))
1980
+
1981
+ let currentCursor: string | undefined = cursor
1982
+ const allItems: Item[] = []
1983
+ let hasMore = false
1984
+ let lastGroup: TxGroup | undefined
1985
+
1986
+ // Fetch pages until the target entry count is reached (or no more rows
1987
+ // exist), up to three attempts. When folding is requested the target is
1988
+ // measured against grouped entries (folding can collapse a page to very few),
1989
+ // otherwise against the raw item count.
1990
+ for (let attempt = 0; attempt < 3; attempt++) {
1991
+ const { rows } = await provider.fetchRows({ address: viewer, cursor: currentCursor, limit })
1992
+ const grouped = groupByTx(rows)
1993
+
1994
+ const txGroups = grouped.slice(0, limit + 1)
1995
+ hasMore = txGroups.length > limit
1996
+ const page = txGroups.slice(0, limit)
1997
+
1998
+ if (page.length === 0) break
1999
+
2000
+ allItems.push(
2001
+ ...page.flatMap((txGroup) =>
2002
+ classifyGroup(txGroup, viewer, fingerprintMap, logs).map(withTitle),
2003
+ ),
2004
+ )
2005
+ lastGroup = page[page.length - 1]
2006
+
2007
+ const enough = group
2008
+ ? groupByAccessKey(allItems).length >= minGroupEntries
2009
+ : allItems.length >= limit
2010
+ if (enough || !hasMore) break
2011
+
2012
+ if (lastGroup) currentCursor = provider.cursorForGroup(lastGroup)
2013
+ }
2014
+
2015
+ let nextCursor: string | null = null
2016
+ if (hasMore && lastGroup) nextCursor = provider.cursorForGroup(lastGroup)
2017
+
2018
+ // Items carry stub tokens (`address`/`amount`); the caller enriches them with
2019
+ // metadata via {@link enrichTokens}.
2020
+ return { items: allItems, nextCursor }
2021
+ }
2022
+
2023
+ export declare namespace list {
2024
+ /** Options for {@link list}. */
2025
+ type Options = {
2026
+ /** Subject account address. */
2027
+ address: Address.Address
2028
+ /** Opaque cursor from a previous page; omit for the head page. */
2029
+ cursor?: string | undefined
2030
+ /** Whether the caller will fold items into access-key groups (tunes the page-fill target). */
2031
+ group?: boolean | undefined
2032
+ /** Attach each item's full transaction `events` capture (decoded + raw logs). */
2033
+ logs?: boolean | undefined
2034
+ /** Target page size (entries after grouping). */
2035
+ limit?: number | undefined
2036
+ /** Cache store for the MPP fingerprint-directory lookup. */
2037
+ store: Store.Store
2038
+ }
2039
+ /** Result of {@link list}. */
2040
+ type ReturnType = {
2041
+ items: Item[]
2042
+ nextCursor: string | null
2043
+ }
2044
+ }
2045
+
2046
+ /**
2047
+ * Creates a TIDX-backed {@link Provider} that runs three sub-queries in
2048
+ * parallel and merges them:
2049
+ * - Q1 (outbound): `receipts ⋈ logs` for txs where the address is `from` or
2050
+ * `fee_payer`, yielding full log rows.
2051
+ * - Q2 (inbound): decoded `Transfer` events to the address.
2052
+ * - Q3 (outbound transfers): decoded `Transfer` events from the address
2053
+ * (catches access-key-signed sends where `tx.from` differs from the address).
2054
+ */
2055
+ export function tidx(deps: tidx.Deps): Provider {
2056
+ const { chainId, tidx: tidxClient } = deps
2057
+
2058
+ function parseCursor(cursor: string): { blockNum: number; txIdx: number } | undefined {
2059
+ // Opaque `[blockNum, txIdx]` keyset cursor, the same encoding every other
2060
+ // list endpoint uses; a malformed/forged cursor decodes to `undefined` and
2061
+ // falls back to the head page.
2062
+ const decoded = Cursor.decode(cursor, ['int', 'int'])
2063
+ if (!decoded) return undefined
2064
+ return { blockNum: decoded[0] as number, txIdx: decoded[1] as number }
2065
+ }
2066
+
2067
+ /**
2068
+ * Q1: every log of each transaction the address sent or paid the fee for,
2069
+ * via a single `receipts ⋈ logs` join filtered on `from OR fee_payer`.
2070
+ *
2071
+ * The cursor uses a two-predicate keyset (`block_num <= N` AND `(block_num <
2072
+ * N OR tx_idx < M)`) rather than a plain `block_num < N`: the `<= N` upper
2073
+ * bound lets the ClickHouse planner seek to the cursor block on the
2074
+ * `(block_num, tx_idx)` primary key instead of scanning from the tip, which
2075
+ * is what avoids planner 422s on busy addresses. Over-fetch `limit * 4`
2076
+ * (never `OFFSET`) because one tx fans out into many log rows that
2077
+ * `groupByTx` collapses back into a single group.
2078
+ */
2079
+ async function queryOutbound(options: {
2080
+ address: Address.Address
2081
+ cursor: { blockNum: number; txIdx: number } | undefined
2082
+ limit: number
2083
+ }): Promise<Row[]> {
2084
+ const { address, cursor, limit } = options
2085
+ const keyset = cursor
2086
+ ? `AND receipts.block_num <= ${cursor.blockNum} AND (receipts.block_num < ${cursor.blockNum} OR receipts.tx_idx < ${cursor.txIdx})`
2087
+ : ''
2088
+ // Raw-table join (no signature-decoded CTE), so inline SQL is cast to `string`.
2089
+ const result = await tidxClient.fetch({
2090
+ chainId,
2091
+ query: `
2092
+ SELECT
2093
+ receipts.tx_hash AS tx_hash,
2094
+ receipts.block_num AS block_num,
2095
+ receipts.block_timestamp AS block_timestamp,
2096
+ receipts.tx_idx AS tx_idx,
2097
+ receipts."from" AS tx_sender,
2098
+ receipts.fee_payer AS fee_payer,
2099
+ logs.address AS log_address,
2100
+ logs.selector AS selector,
2101
+ logs.topic1 AS topic1,
2102
+ logs.topic2 AS topic2,
2103
+ logs.topic3 AS topic3,
2104
+ logs.data AS data,
2105
+ logs.log_idx AS log_idx
2106
+ FROM receipts
2107
+ INNER JOIN logs ON logs.tx_hash = receipts.tx_hash
2108
+ WHERE (receipts."from" = '${address}' OR receipts.fee_payer = '${address}') ${keyset}
2109
+ ORDER BY receipts.block_num DESC, receipts.tx_idx DESC
2110
+ LIMIT ${limit * 4}
2111
+ ` as string,
2112
+ })
2113
+
2114
+ return result.rows.map((r) => ({
2115
+ blockNum: Number(Value.toNumber(r['block_num'])),
2116
+ blockTimestamp: Number(Value.toNumber(r['block_timestamp'])),
2117
+ data: Value.toText(r['data']) ?? null,
2118
+ feePayer: lowerOrNull(r['fee_payer']),
2119
+ logAddress: lower(r['log_address']),
2120
+ logIdx: Number(Value.toNumber(r['log_idx'])),
2121
+ selector: Value.toText(r['selector']) ?? null,
2122
+ source: 'q1' as const,
2123
+ topic1: Value.toText(r['topic1']) ?? null,
2124
+ topic2: Value.toText(r['topic2']) ?? null,
2125
+ topic3: Value.toText(r['topic3']) ?? null,
2126
+ txHash: lowerHash(r['tx_hash']),
2127
+ txIdx: Number(Value.toNumber(r['tx_idx'])),
2128
+ txSender: lowerOrNull(r['tx_sender']),
2129
+ }))
2130
+ }
2131
+
2132
+ /** Q2/Q3: decoded `Transfer` events on one side of the address. */
2133
+ async function queryTransfers(options: {
2134
+ address: Address.Address
2135
+ cursor: { blockNum: number; txIdx: number } | undefined
2136
+ limit: number
2137
+ side: 'inbound' | 'outbound'
2138
+ }): Promise<Row[]> {
2139
+ const { address, cursor, limit, side } = options
2140
+ const where =
2141
+ side === 'inbound'
2142
+ ? `"to" = '${address}' AND "from" != '${address}'`
2143
+ : `"from" = '${address}' AND "to" != '${address}'`
2144
+ const keyset = cursor ? `AND block_num < ${cursor.blockNum}` : ''
2145
+ const result = await tidxClient.fetch({
2146
+ chainId,
2147
+ query: `
2148
+ SELECT "from", "to", address, value, tx_hash, block_num, block_timestamp, log_idx, tx_idx
2149
+ FROM Transfer
2150
+ WHERE ${where} ${keyset}
2151
+ ORDER BY block_num DESC, tx_idx DESC
2152
+ LIMIT ${limit * 4}
2153
+ `,
2154
+ signatures: [transferSignature],
2155
+ })
2156
+ return result.rows.map((r) => ({
2157
+ blockNum: Number(Value.toNumber(r['block_num'])),
2158
+ blockTimestamp: Number(Value.toNumber(r['block_timestamp'])),
2159
+ data: null,
2160
+ feePayer: null,
2161
+ logAddress: lower(r['address']),
2162
+ logIdx: Number(Value.toNumber(r['log_idx'])),
2163
+ selector: null,
2164
+ source: 'q2' as const,
2165
+ topic1: null,
2166
+ topic2: null,
2167
+ topic3: null,
2168
+ transferAmount: BigInt(Value.toIntegerString(r['value']) ?? '0'),
2169
+ transferFrom: lower(r['from']),
2170
+ transferTo: lower(r['to']),
2171
+ transferToken: lower(r['address']),
2172
+ txHash: lowerHash(r['tx_hash']),
2173
+ txIdx: Number(Value.toNumber(r['tx_idx'])),
2174
+ txSender: null,
2175
+ }))
2176
+ }
2177
+
2178
+ return {
2179
+ cursorForGroup(group) {
2180
+ return Cursor.encode([group.blockNum, group.txIdx])
2181
+ },
2182
+ async fetchRows({ address, cursor, limit }) {
2183
+ const parsed = cursor ? parseCursor(cursor) : undefined
2184
+ const [q1, q2, q3] = await Promise.all([
2185
+ queryOutbound({ address, cursor: parsed, limit }),
2186
+ queryTransfers({ address, cursor: parsed, limit, side: 'inbound' }),
2187
+ queryTransfers({ address, cursor: parsed, limit, side: 'outbound' }),
2188
+ ])
2189
+ return { rows: mergeAndSort(q1, q2, q3) }
2190
+ },
2191
+ }
2192
+ }
2193
+
2194
+ export declare namespace tidx {
2195
+ /** Dependencies for {@link tidx}. */
2196
+ type Deps = {
2197
+ /** Source chain id. */
2198
+ chainId: number
2199
+ /** TIDX query client. */
2200
+ tidx: Tidx.Client
2201
+ }
2202
+ }
2203
+
2204
+ /**
2205
+ * Merges three sub-query results into one `(blockNum desc, txIdx desc)` stream.
2206
+ * Q2/Q3 rows whose hash already appears in Q1 are dropped (Q1 rows are richer),
2207
+ * and Q3 is also deduped against Q2.
2208
+ */
2209
+ export function mergeAndSort(q1: readonly Row[], q2: readonly Row[], q3: readonly Row[]): Row[] {
2210
+ const q1Hashes = new Set(q1.map((r) => r.txHash))
2211
+ const filteredQ2 = q2.filter((r) => !q1Hashes.has(r.txHash))
2212
+ const filteredQ3 = q3.filter((r) => !q1Hashes.has(r.txHash))
2213
+ const q2Hashes = new Set(filteredQ2.map((r) => r.txHash))
2214
+ const dedupedQ3 = filteredQ3.filter((r) => !q2Hashes.has(r.txHash))
2215
+ return [...q1, ...filteredQ2, ...dedupedQ3].sort((a, b) => {
2216
+ if (a.blockNum !== b.blockNum) return b.blockNum - a.blockNum
2217
+ return b.txIdx - a.txIdx
2218
+ })
2219
+ }
2220
+
2221
+ /** Groups rows by transaction hash, preserving sort order. */
2222
+ function groupByTx(rows: Row[]): TxGroup[] {
2223
+ const map = new Map<string, TxGroup>()
2224
+ const order: string[] = []
2225
+
2226
+ for (const row of rows) {
2227
+ const key = row.txHash
2228
+ let group = map.get(key)
2229
+ if (!group) {
2230
+ group = {
2231
+ blockNum: row.blockNum,
2232
+ blockTimestamp: row.blockTimestamp,
2233
+ feePayer: row.feePayer,
2234
+ rows: [],
2235
+ txHash: row.txHash,
2236
+ txIdx: row.txIdx,
2237
+ txSender: row.txSender,
2238
+ }
2239
+ map.set(key, group)
2240
+ order.push(key)
2241
+ }
2242
+ // Inherit tx sender / fee payer from richer Q1 rows when present.
2243
+ if (row.txSender && !group.txSender) group.txSender = row.txSender
2244
+ if (row.feePayer && !group.feePayer) group.feePayer = row.feePayer
2245
+ group.rows.push(row)
2246
+ }
2247
+
2248
+ return order.map((key) => map.get(key)!)
2249
+ }
2250
+
2251
+ /**
2252
+ * Classifies a transaction group into one or more activity items. Q1 groups go
2253
+ * through full log decoding; Q2-only groups (transfers without a joined receipt)
2254
+ * are classified directly from the transfer columns.
2255
+ */
2256
+ function classifyGroup(
2257
+ group: TxGroup,
2258
+ viewer: Address.Address,
2259
+ fingerprintMap: Record<string, string>,
2260
+ includeLogs = false,
2261
+ ): RawItem[] {
2262
+ const { blockNum, blockTimestamp, feePayer, txHash, txSender } = group
2263
+ // `blockTimestamp` is epoch seconds; the public feed uses ISO 8601 like every
2264
+ // other resource's `timestamp`.
2265
+ const timestamp = new Date(Number(blockTimestamp) * 1000).toISOString()
2266
+ // `outgoing` when the viewer initiated the transaction (sent it or paid the
2267
+ // fee); `incoming` when it merely happened to them (someone else initiated it).
2268
+ const perspective: 'incoming' | 'outgoing' =
2269
+ (txSender && Address.isEqual(txSender, viewer)) ||
2270
+ (feePayer && Address.isEqual(feePayer, viewer))
2271
+ ? 'outgoing'
2272
+ : 'incoming'
2273
+ const itemBase = (logIndex: number) =>
2274
+ baseFor({ logIndex, perspective, timestamp, transactionHash: txHash })
2275
+ const signer: 'self' | Address.Address =
2276
+ txSender && Address.isEqual(txSender, viewer) ? 'self' : (txSender ?? 'self')
2277
+
2278
+ const q1Rows = group.rows.filter((r) => r.source === 'q1')
2279
+ const q2Rows = group.rows.filter((r) => r.source === 'q2')
2280
+
2281
+ if (q1Rows.length > 0)
2282
+ return classifyFromLogs(q1Rows, {
2283
+ blockNum,
2284
+ fingerprintMap,
2285
+ includeLogs,
2286
+ itemBase,
2287
+ signer,
2288
+ viewer,
2289
+ })
2290
+
2291
+ // Q2-only — a transfer row without a joined receipt. Classify by comparing
2292
+ // `from`/`to` against the viewer (zero-address sides are mints/burns).
2293
+ if (q2Rows.length > 0) {
2294
+ const r = q2Rows[0]!
2295
+ const base = itemBase(r.logIdx)
2296
+ const from = r.transferFrom!
2297
+ const to = r.transferTo!
2298
+ const sourceToken = token(r.transferAmount, r.transferToken!)
2299
+ if (Address.isEqual(from, zeroAddress as Address.Address))
2300
+ return [{ ...base, data: { recipient: viewer, sourceToken }, type: 'mint' }]
2301
+ if (Address.isEqual(to, zeroAddress as Address.Address))
2302
+ return [{ ...base, data: { sender: from, signer, sourceToken }, type: 'burn' }]
2303
+ // Q2-only rows carry no memo, so MPP service attribution is unavailable
2304
+ // here; `attribution` is resolved only on the log-bearing (Q1) path.
2305
+ if (Address.isEqual(from, viewer))
2306
+ return [
2307
+ transfer(base, blockNum, {
2308
+ direction: 'out',
2309
+ recipient: to,
2310
+ sender: viewer,
2311
+ signer,
2312
+ sourceToken,
2313
+ }),
2314
+ ]
2315
+ return [
2316
+ transfer(base, blockNum, { direction: 'in', recipient: viewer, sender: from, sourceToken }),
2317
+ ]
2318
+ }
2319
+
2320
+ // Fallback — should not happen (a group with neither q1 nor q2 rows).
2321
+ return [
2322
+ unknown(itemBase(Math.min(...group.rows.map((r) => r.logIdx))), {
2323
+ events: buildTxEvents(group.rows, []),
2324
+ signer,
2325
+ }),
2326
+ ]
2327
+ }
2328
+
2329
+ /** Classifies a Q1 (log-bearing) group via `parseEventLogs`. */
2330
+ function classifyFromLogs(
2331
+ rows: Row[],
2332
+ ctx: {
2333
+ blockNum: number
2334
+ fingerprintMap: Record<string, string>
2335
+ includeLogs: boolean
2336
+ itemBase: (logIndex: number) => Base
2337
+ signer: 'self' | Address.Address
2338
+ viewer: Address.Address
2339
+ },
2340
+ ): RawItem[] {
2341
+ const items = classifyFromLogsCore(rows, ctx)
2342
+ // When the caller requested raw logs, attach the full per-transaction log set
2343
+ // (decoded when possible) to every classified item so no co-located log is
2344
+ // hidden behind the headline. `unknown` items already carry it.
2345
+ if (!ctx.includeLogs) return items
2346
+ const events = buildTxEvents(rows, decodeRows(rows))
2347
+ return items.map((item) => (item.type === 'unknown' ? item : { ...item, events }))
2348
+ }
2349
+
2350
+ /** Classifies a Q1 (log-bearing) group via `parseEventLogs`, picking one headline item. */
2351
+ function classifyFromLogsCore(
2352
+ rows: Row[],
2353
+ ctx: {
2354
+ blockNum: number
2355
+ fingerprintMap: Record<string, string>
2356
+ itemBase: (logIndex: number) => Base
2357
+ signer: 'self' | Address.Address
2358
+ viewer: Address.Address
2359
+ },
2360
+ ): RawItem[] {
2361
+ const { blockNum, fingerprintMap, itemBase, viewer } = ctx
2362
+
2363
+ // An access-key-signed tx emits AccessKeySpend at the accountKeychain; topic2
2364
+ // is the access-key publicKey. Use it as the signer so successive access-key
2365
+ // actions group together.
2366
+ const accessKeySpendRow = rows.find(
2367
+ (r) =>
2368
+ r.selector === accessKeySpendSelector &&
2369
+ Address.isEqual(r.logAddress, Addresses.accountKeychain),
2370
+ )
2371
+ const accessKeySigner = accessKeySpendRow?.topic2
2372
+ ? (`0x${accessKeySpendRow.topic2.slice(-40)}`.toLowerCase() as Address.Address)
2373
+ : null
2374
+ const signer: 'self' | Address.Address =
2375
+ accessKeySigner && !Address.isEqual(accessKeySigner, viewer) ? accessKeySigner : ctx.signer
2376
+
2377
+ const events = decodeRows(rows)
2378
+
2379
+ const minLogIndex = Math.min(...rows.map((r) => r.logIdx))
2380
+
2381
+ // Payment-channel settlement: a CloseRequested topic plus an optional refund
2382
+ // transfer back to the viewer.
2383
+ const hasCloseRequested = rows.some((r) => r.selector === closeRequestedSelector)
2384
+ if (hasCloseRequested) {
2385
+ const closeRequestedLogIndex =
2386
+ rows.find((r) => r.selector === closeRequestedSelector)?.logIdx ?? minLogIndex
2387
+ const refundTransfer = events.find(
2388
+ (e) =>
2389
+ (e.eventName === 'Transfer' || e.eventName === 'TransferWithMemo') &&
2390
+ 'to' in e.args &&
2391
+ Address.isEqual((e.args as { to: Address.Address }).to, viewer),
2392
+ )
2393
+ const refund = refundTransfer
2394
+ ? token(
2395
+ (refundTransfer.args as { amount: bigint }).amount,
2396
+ refundTransfer.address as Address.Address,
2397
+ )
2398
+ : undefined
2399
+ return [
2400
+ {
2401
+ ...itemBase(closeRequestedLogIndex),
2402
+ data: { ...(refund ? { refund } : {}), signer },
2403
+ type: 'session-closed',
2404
+ },
2405
+ ]
2406
+ }
2407
+
2408
+ // Dedup preference: TransferWithMemo beats Transfer; Mint/Burn beat
2409
+ // zero-address Transfer.
2410
+ const preferenceMap = new Map<string, string>()
2411
+ for (const event of events) {
2412
+ if (event.eventName === 'TransferWithMemo' && 'from' in event.args && 'to' in event.args) {
2413
+ const args = event.args as { amount: bigint; from: Address.Address; to: Address.Address }
2414
+ preferenceMap.set(`${args.from}:${args.to}:${args.amount}`, 'TransferWithMemo')
2415
+ }
2416
+ if (event.eventName === 'Mint' && 'amount' in event.args)
2417
+ preferenceMap.set(
2418
+ `mint:${event.address}:${(event.args as { amount: bigint }).amount}`,
2419
+ 'Mint',
2420
+ )
2421
+ if (event.eventName === 'Burn' && 'amount' in event.args)
2422
+ preferenceMap.set(
2423
+ `burn:${event.address}:${(event.args as { amount: bigint }).amount}`,
2424
+ 'Burn',
2425
+ )
2426
+ }
2427
+
2428
+ const dedupedEvents = events.filter((event) => {
2429
+ if (event.eventName !== 'Transfer') return true
2430
+ const args = event.args as { amount: bigint; from: Address.Address; to: Address.Address }
2431
+ if (preferenceMap.has(`${args.from}:${args.to}:${args.amount}`)) return false
2432
+ if (
2433
+ Address.isEqual(args.from, zeroAddress as Address.Address) &&
2434
+ preferenceMap.has(`mint:${event.address}:${args.amount}`)
2435
+ )
2436
+ return false
2437
+ if (
2438
+ Address.isEqual(args.to, zeroAddress as Address.Address) &&
2439
+ preferenceMap.has(`burn:${event.address}:${args.amount}`)
2440
+ )
2441
+ return false
2442
+ return true
2443
+ })
2444
+
2445
+ const finalEvents = dedupedEvents.filter((event) => {
2446
+ if (event.eventName !== 'TransferWithMemo') return true
2447
+ const args = event.args as { amount: bigint; from: Address.Address; to: Address.Address }
2448
+ if (
2449
+ Address.isEqual(args.from, zeroAddress as Address.Address) &&
2450
+ preferenceMap.has(`mint:${event.address}:${args.amount}`)
2451
+ )
2452
+ return false
2453
+ if (
2454
+ Address.isEqual(args.to, zeroAddress as Address.Address) &&
2455
+ preferenceMap.has(`burn:${event.address}:${args.amount}`)
2456
+ )
2457
+ return false
2458
+ return true
2459
+ })
2460
+
2461
+ const transfers = finalEvents.filter(
2462
+ (e) =>
2463
+ (e.eventName === 'Transfer' || e.eventName === 'TransferWithMemo') &&
2464
+ 'to' in e.args &&
2465
+ 'from' in e.args &&
2466
+ 'amount' in e.args,
2467
+ )
2468
+
2469
+ // Swap: a transfer into the DEX paired with one out of the DEX.
2470
+ const toDex = transfers.find((e) =>
2471
+ Address.isEqual((e.args as { to: Address.Address }).to, Addresses.stablecoinDex),
2472
+ )
2473
+ const fromDex = transfers.find((e) =>
2474
+ Address.isEqual((e.args as { from: Address.Address }).from, Addresses.stablecoinDex),
2475
+ )
2476
+ if (toDex && fromDex) {
2477
+ const swapper = (fromDex.args as { to: Address.Address }).to
2478
+ const destTokenAddress = fromDex.address as Address.Address
2479
+ const sourceToken = token(
2480
+ (toDex.args as { amount: bigint }).amount,
2481
+ toDex.address as Address.Address,
2482
+ )
2483
+
2484
+ // Cross-token transfer: the swapped-out token is forwarded from the swapper
2485
+ // to a different recipient in the same transaction (the account server's
2486
+ // approve+swap+transfer multicall). Fold the swap and its delivery leg into
2487
+ // one `transfer` carrying both the source token spent into the DEX and the
2488
+ // destination token delivered to the recipient. A swap whose output stays
2489
+ // with the swapper (no onward leg) stays a plain `swap` below.
2490
+ const delivery = transfers.find(
2491
+ (e) =>
2492
+ Address.isEqual(e.address, destTokenAddress) &&
2493
+ Address.isEqual((e.args as { from: Address.Address }).from, swapper) &&
2494
+ !Address.isEqual((e.args as { to: Address.Address }).to, Addresses.stablecoinDex) &&
2495
+ !Address.isEqual((e.args as { to: Address.Address }).to, swapper),
2496
+ )
2497
+ if (delivery) {
2498
+ const args = delivery.args as {
2499
+ amount: bigint
2500
+ from: Address.Address
2501
+ memo?: Hex.Hex | undefined
2502
+ to: Address.Address
2503
+ }
2504
+ const rawMemo = delivery.eventName === 'TransferWithMemo' ? args.memo : undefined
2505
+ const memo = rawMemo ? Transfers.memoToString(rawMemo) : undefined
2506
+ const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo })
2507
+ return [
2508
+ transfer(itemBase(logIndexOfEvent(delivery)), blockNum, {
2509
+ attribution,
2510
+ destinationToken: token(args.amount, delivery.address as Address.Address),
2511
+ direction: Address.isEqual(args.to, viewer) ? 'in' : 'out',
2512
+ memo,
2513
+ recipient: args.to.toLowerCase() as Address.Address,
2514
+ sender: swapper.toLowerCase() as Address.Address,
2515
+ signer,
2516
+ sourceToken,
2517
+ }),
2518
+ ]
2519
+ }
2520
+
2521
+ return [
2522
+ {
2523
+ ...itemBase(Math.min(logIndexOfEvent(toDex), logIndexOfEvent(fromDex))),
2524
+ data: {
2525
+ destinationToken: token((fromDex.args as { amount: bigint }).amount, destTokenAddress),
2526
+ signer,
2527
+ sourceToken,
2528
+ },
2529
+ type: 'swap',
2530
+ },
2531
+ ]
2532
+ }
2533
+
2534
+ // Mint: explicit `Mint` event (not the fee manager) or a transfer from zero.
2535
+ const mintEvent = finalEvents.find(
2536
+ (e) =>
2537
+ e.eventName === 'Mint' &&
2538
+ 'amount' in e.args &&
2539
+ !Address.isEqual(e.address, Addresses.feeManager),
2540
+ )
2541
+ if (mintEvent) {
2542
+ const args = mintEvent.args as { amount: bigint; to: Address.Address }
2543
+ return [
2544
+ {
2545
+ ...itemBase(logIndexOfEvent(mintEvent)),
2546
+ data: {
2547
+ recipient: args.to.toLowerCase() as Address.Address,
2548
+ sourceToken: token(args.amount, mintEvent.address as Address.Address),
2549
+ },
2550
+ type: 'mint',
2551
+ },
2552
+ ]
2553
+ }
2554
+ const zeroTransferIn = transfers.find((e) =>
2555
+ Address.isEqual((e.args as { from: Address.Address }).from, zeroAddress as Address.Address),
2556
+ )
2557
+ if (zeroTransferIn) {
2558
+ const args = zeroTransferIn.args as { amount: bigint; to: Address.Address }
2559
+ return [
2560
+ {
2561
+ ...itemBase(logIndexOfEvent(zeroTransferIn)),
2562
+ data: {
2563
+ recipient: (args.to as string).toLowerCase() as Address.Address,
2564
+ sourceToken: token(args.amount, zeroTransferIn.address as Address.Address),
2565
+ },
2566
+ type: 'mint',
2567
+ },
2568
+ ]
2569
+ }
2570
+
2571
+ // Burn blocked: a compliance burn of receive-policy-blocked funds. It also
2572
+ // emits a zero-address `Transfer` that would otherwise classify as `burn`, so
2573
+ // detect the distinct `BurnBlocked` event first.
2574
+ const burnBlockedEvent = finalEvents.find((e) => e.eventName === 'BurnBlocked')
2575
+ if (burnBlockedEvent) {
2576
+ const args = burnBlockedEvent.args as { amount: bigint; from: Address.Address }
2577
+ return [
2578
+ {
2579
+ ...itemBase(logIndexOfEvent(burnBlockedEvent)),
2580
+ data: {
2581
+ sender: args.from.toLowerCase() as Address.Address,
2582
+ signer,
2583
+ sourceToken: token(args.amount, burnBlockedEvent.address as Address.Address),
2584
+ },
2585
+ type: 'burn-blocked',
2586
+ },
2587
+ ]
2588
+ }
2589
+
2590
+ // Burn: explicit `Burn` event or a transfer to zero.
2591
+ const burnEvent = finalEvents.find((e) => e.eventName === 'Burn' && 'amount' in e.args)
2592
+ if (burnEvent) {
2593
+ const args = burnEvent.args as { amount: bigint; from: Address.Address }
2594
+ return [
2595
+ {
2596
+ ...itemBase(logIndexOfEvent(burnEvent)),
2597
+ data: {
2598
+ sender: args.from.toLowerCase() as Address.Address,
2599
+ signer,
2600
+ sourceToken: token(args.amount, burnEvent.address as Address.Address),
2601
+ },
2602
+ type: 'burn',
2603
+ },
2604
+ ]
2605
+ }
2606
+ const zeroTransferOut = transfers.find((e) =>
2607
+ Address.isEqual((e.args as { to: Address.Address }).to, zeroAddress as Address.Address),
2608
+ )
2609
+ if (zeroTransferOut) {
2610
+ const args = zeroTransferOut.args as { amount: bigint; from: Address.Address }
2611
+ return [
2612
+ {
2613
+ ...itemBase(logIndexOfEvent(zeroTransferOut)),
2614
+ data: {
2615
+ sender: (args.from as string).toLowerCase() as Address.Address,
2616
+ signer,
2617
+ sourceToken: token(args.amount, zeroTransferOut.address as Address.Address),
2618
+ },
2619
+ type: 'burn',
2620
+ },
2621
+ ]
2622
+ }
2623
+
2624
+ // Approval (excluding zone-outbox spenders).
2625
+ const approvalEvent = finalEvents.find(
2626
+ (e) =>
2627
+ e.eventName === 'Approval' &&
2628
+ !Address.isEqual((e.args as { spender: Address.Address }).spender, Addresses.zoneOutbox),
2629
+ )
2630
+ if (approvalEvent) {
2631
+ const args = approvalEvent.args as { amount: bigint; spender: Address.Address }
2632
+ return [
2633
+ {
2634
+ ...itemBase(logIndexOfEvent(approvalEvent)),
2635
+ data: {
2636
+ signer,
2637
+ sourceToken: token(args.amount, approvalEvent.address as Address.Address),
2638
+ spender: args.spender.toLowerCase() as Address.Address,
2639
+ },
2640
+ type: 'approval',
2641
+ },
2642
+ ]
2643
+ }
2644
+
2645
+ // Precompile-event activities. These run before the generic transfer
2646
+ // fallback because each is emitted alongside a co-located TIP-20 `Transfer`
2647
+ // whose counterparty is the precompile (the DEX, the channel reserve, the fee
2648
+ // manager, the token itself), which would otherwise classify as a plain
2649
+ // transfer. They are grouped by (transaction + precompile counterparty), not
2650
+ // by log adjacency.
2651
+
2652
+ // Payment channel opened: `ChannelOpened` carries the deposit token/amount
2653
+ // directly, so no paired-transfer lookup is needed.
2654
+ const channelOpened = finalEvents.find((e) => e.eventName === 'ChannelOpened')
2655
+ if (channelOpened) {
2656
+ const args = channelOpened.args as {
2657
+ channelId: Hex.Hex
2658
+ deposit: bigint
2659
+ payee: Address.Address
2660
+ payer: Address.Address
2661
+ token: Address.Address
2662
+ }
2663
+ return [
2664
+ {
2665
+ ...itemBase(logIndexOfEvent(channelOpened)),
2666
+ data: {
2667
+ channelId: args.channelId.toLowerCase() as Hex.Hex,
2668
+ payee: args.payee.toLowerCase() as Address.Address,
2669
+ payer: args.payer.toLowerCase() as Address.Address,
2670
+ signer,
2671
+ sourceToken: token(args.deposit, args.token),
2672
+ },
2673
+ type: 'channel-opened',
2674
+ },
2675
+ ]
2676
+ }
2677
+
2678
+ // The channel reserve doesn't put the token on `TopUp`/`Settled`/
2679
+ // `ChannelClosed`, so recover it from the co-located funding/payout transfer.
2680
+ const reserveInflow = transfers.find((e) =>
2681
+ Address.isEqual((e.args as { to: Address.Address }).to, tip20ChannelReserve),
2682
+ )
2683
+ const reserveOutflowTo = (recipient: Address.Address) =>
2684
+ transfers.find(
2685
+ (e) =>
2686
+ Address.isEqual((e.args as { from: Address.Address }).from, tip20ChannelReserve) &&
2687
+ Address.isEqual((e.args as { to: Address.Address }).to, recipient),
2688
+ )
2689
+
2690
+ // Payment channel topped up: `additionalDeposit` from the event, token from
2691
+ // the inbound funding transfer to the reserve.
2692
+ const topUp = finalEvents.find((e) => e.eventName === 'TopUp')
2693
+ if (topUp) {
2694
+ const args = topUp.args as { additionalDeposit: bigint; channelId: Hex.Hex }
2695
+ const tokenAddress = reserveInflow
2696
+ ? (reserveInflow.address as Address.Address)
2697
+ : (zeroAddress as Address.Address)
2698
+ return [
2699
+ {
2700
+ ...itemBase(logIndexOfEvent(topUp)),
2701
+ data: {
2702
+ channelId: args.channelId.toLowerCase() as Hex.Hex,
2703
+ signer,
2704
+ sourceToken: token(args.additionalDeposit, tokenAddress),
2705
+ },
2706
+ type: 'channel-funded',
2707
+ },
2708
+ ]
2709
+ }
2710
+
2711
+ // Payment channel settled: `deltaPaid` from the event, token from the payout
2712
+ // transfer out of the reserve to the payee.
2713
+ const settled = finalEvents.find((e) => e.eventName === 'Settled')
2714
+ if (settled) {
2715
+ const args = settled.args as {
2716
+ channelId: Hex.Hex
2717
+ deltaPaid: bigint
2718
+ payee: Address.Address
2719
+ payer: Address.Address
2720
+ }
2721
+ const payeeLeg = reserveOutflowTo(args.payee.toLowerCase() as Address.Address)
2722
+ const tokenAddress = payeeLeg
2723
+ ? (payeeLeg.address as Address.Address)
2724
+ : (zeroAddress as Address.Address)
2725
+ return [
2726
+ {
2727
+ ...itemBase(logIndexOfEvent(settled)),
2728
+ data: {
2729
+ channelId: args.channelId.toLowerCase() as Hex.Hex,
2730
+ payee: args.payee.toLowerCase() as Address.Address,
2731
+ payer: args.payer.toLowerCase() as Address.Address,
2732
+ signer,
2733
+ sourceToken: token(args.deltaPaid, tokenAddress),
2734
+ },
2735
+ type: 'channel-settled',
2736
+ },
2737
+ ]
2738
+ }
2739
+
2740
+ // Payment channel closed: payout to the payee and refund to the payer arrive
2741
+ // as up to two transfers out of the reserve; attach whichever are present.
2742
+ const channelClosed = finalEvents.find((e) => e.eventName === 'ChannelClosed')
2743
+ if (channelClosed) {
2744
+ const args = channelClosed.args as {
2745
+ channelId: Hex.Hex
2746
+ payee: Address.Address
2747
+ payer: Address.Address
2748
+ refundedToPayer: bigint
2749
+ settledToPayee: bigint
2750
+ }
2751
+ const payeeLeg = reserveOutflowTo(args.payee.toLowerCase() as Address.Address)
2752
+ const payerLeg = reserveOutflowTo(args.payer.toLowerCase() as Address.Address)
2753
+ return [
2754
+ {
2755
+ ...itemBase(logIndexOfEvent(channelClosed)),
2756
+ data: {
2757
+ channelId: args.channelId.toLowerCase() as Hex.Hex,
2758
+ payee: args.payee.toLowerCase() as Address.Address,
2759
+ payer: args.payer.toLowerCase() as Address.Address,
2760
+ ...(payerLeg && args.refundedToPayer > 0n
2761
+ ? { refund: token(args.refundedToPayer, payerLeg.address as Address.Address) }
2762
+ : {}),
2763
+ signer,
2764
+ ...(payeeLeg
2765
+ ? { sourceToken: token(args.settledToPayee, payeeLeg.address as Address.Address) }
2766
+ : {}),
2767
+ },
2768
+ type: 'channel-closed',
2769
+ },
2770
+ ]
2771
+ }
2772
+
2773
+ // Payment channel close cancelled: the payer revoked a pending close request.
2774
+ // When bundled with a top-up the `TopUp` classifier above wins; on its own
2775
+ // this carries no transfer, so the event itself is the signal.
2776
+ const closeCancelled = finalEvents.find((e) => e.eventName === 'CloseRequestCancelled')
2777
+ if (closeCancelled) {
2778
+ const args = closeCancelled.args as {
2779
+ channelId: Hex.Hex
2780
+ payee: Address.Address
2781
+ payer: Address.Address
2782
+ }
2783
+ return [
2784
+ {
2785
+ ...itemBase(logIndexOfEvent(closeCancelled)),
2786
+ data: {
2787
+ channelId: args.channelId.toLowerCase() as Hex.Hex,
2788
+ payee: args.payee.toLowerCase() as Address.Address,
2789
+ payer: args.payer.toLowerCase() as Address.Address,
2790
+ signer,
2791
+ },
2792
+ type: 'channel-close-cancelled',
2793
+ },
2794
+ ]
2795
+ }
2796
+
2797
+ // DEX order placed: `OrderPlaced` carries the escrow token/amount and side.
2798
+ // (Auto-flip re-places are inside swaps, which already returned above.)
2799
+ const orderPlaced = finalEvents.find((e) => e.eventName === 'OrderPlaced')
2800
+ if (orderPlaced) {
2801
+ const args = orderPlaced.args as {
2802
+ amount: bigint
2803
+ isBid: boolean
2804
+ orderId: bigint
2805
+ tick: number
2806
+ token: Address.Address
2807
+ }
2808
+ return [
2809
+ {
2810
+ ...itemBase(logIndexOfEvent(orderPlaced)),
2811
+ data: {
2812
+ orderId: args.orderId.toString(),
2813
+ side: args.isBid ? 'bid' : 'ask',
2814
+ signer,
2815
+ sourceToken: token(args.amount, args.token),
2816
+ tick: Number(args.tick),
2817
+ },
2818
+ type: 'order-placed',
2819
+ },
2820
+ ]
2821
+ }
2822
+
2823
+ // DEX order cancelled: refund is credited to the internal DEX balance, so
2824
+ // there is no co-located transfer — the `OrderCancelled` event is the signal.
2825
+ const orderCancelled = finalEvents.find((e) => e.eventName === 'OrderCancelled')
2826
+ if (orderCancelled) {
2827
+ const args = orderCancelled.args as { orderId: bigint }
2828
+ return [
2829
+ {
2830
+ ...itemBase(logIndexOfEvent(orderCancelled)),
2831
+ data: { orderId: args.orderId.toString(), signer },
2832
+ type: 'order-cancelled',
2833
+ },
2834
+ ]
2835
+ }
2836
+
2837
+ // Fee-AMM rebalance swap: `validatorToken` in, `userToken` out.
2838
+ const rebalanceSwap = finalEvents.find((e) => e.eventName === 'RebalanceSwap')
2839
+ if (rebalanceSwap) {
2840
+ const args = rebalanceSwap.args as {
2841
+ amountIn: bigint
2842
+ amountOut: bigint
2843
+ swapper: Address.Address
2844
+ userToken: Address.Address
2845
+ validatorToken: Address.Address
2846
+ }
2847
+ return [
2848
+ {
2849
+ ...itemBase(logIndexOfEvent(rebalanceSwap)),
2850
+ data: {
2851
+ destinationToken: token(args.amountOut, args.userToken),
2852
+ signer,
2853
+ sourceToken: token(args.amountIn, args.validatorToken),
2854
+ swapper: args.swapper.toLowerCase() as Address.Address,
2855
+ },
2856
+ type: 'fee-rebalance-swap',
2857
+ },
2858
+ ]
2859
+ }
2860
+
2861
+ // Validator fees distributed.
2862
+ const feesDistributed = finalEvents.find((e) => e.eventName === 'FeesDistributed')
2863
+ if (feesDistributed) {
2864
+ const args = feesDistributed.args as {
2865
+ amount: bigint
2866
+ token: Address.Address
2867
+ validator: Address.Address
2868
+ }
2869
+ return [
2870
+ {
2871
+ ...itemBase(logIndexOfEvent(feesDistributed)),
2872
+ data: {
2873
+ signer,
2874
+ sourceToken: token(args.amount, args.token),
2875
+ validator: args.validator.toLowerCase() as Address.Address,
2876
+ },
2877
+ type: 'fees-distributed',
2878
+ },
2879
+ ]
2880
+ }
2881
+
2882
+ // Reward pool funded: the funded token is the emitting token contract itself.
2883
+ const rewardDistributed = finalEvents.find((e) => e.eventName === 'RewardDistributed')
2884
+ if (rewardDistributed) {
2885
+ const args = rewardDistributed.args as { amount: bigint; funder: Address.Address }
2886
+ return [
2887
+ {
2888
+ ...itemBase(logIndexOfEvent(rewardDistributed)),
2889
+ data: {
2890
+ funder: args.funder.toLowerCase() as Address.Address,
2891
+ signer,
2892
+ sourceToken: token(args.amount, rewardDistributed.address as Address.Address),
2893
+ },
2894
+ type: 'reward-distributed',
2895
+ },
2896
+ ]
2897
+ }
2898
+
2899
+ // Reward recipient redirected.
2900
+ const rewardRecipientSet = finalEvents.find((e) => e.eventName === 'RewardRecipientSet')
2901
+ if (rewardRecipientSet) {
2902
+ const args = rewardRecipientSet.args as { holder: Address.Address; recipient: Address.Address }
2903
+ return [
2904
+ {
2905
+ ...itemBase(logIndexOfEvent(rewardRecipientSet)),
2906
+ data: {
2907
+ holder: args.holder.toLowerCase() as Address.Address,
2908
+ recipient: args.recipient.toLowerCase() as Address.Address,
2909
+ signer,
2910
+ },
2911
+ type: 'reward-recipient-set',
2912
+ },
2913
+ ]
2914
+ }
2915
+
2916
+ // Access-key spending limit updated: `newLimit` is carried as the token
2917
+ // amount so it enriches with the token's metadata.
2918
+ const spendingLimitUpdated = finalEvents.find((e) => e.eventName === 'SpendingLimitUpdated')
2919
+ if (spendingLimitUpdated) {
2920
+ const args = spendingLimitUpdated.args as {
2921
+ account: Address.Address
2922
+ newLimit: bigint
2923
+ publicKey: Address.Address
2924
+ token: Address.Address
2925
+ }
2926
+ return [
2927
+ {
2928
+ ...itemBase(logIndexOfEvent(spendingLimitUpdated)),
2929
+ data: {
2930
+ account: args.account.toLowerCase() as Address.Address,
2931
+ publicKey: args.publicKey.toLowerCase() as Address.Address,
2932
+ signer,
2933
+ sourceToken: token(args.newLimit, args.token),
2934
+ },
2935
+ type: 'spending-limit-updated',
2936
+ },
2937
+ ]
2938
+ }
2939
+
2940
+ // Access-key lifecycle events — emitted alongside any co-located transfer.
2941
+ const accessKeyItems: RawItem[] = []
2942
+ for (const event of finalEvents) {
2943
+ if (
2944
+ event.eventName === 'KeyAuthorized' &&
2945
+ Address.isEqual(event.address, Addresses.accountKeychain)
2946
+ ) {
2947
+ const args = event.args as {
2948
+ account: Address.Address
2949
+ expiry: bigint
2950
+ publicKey: Address.Address
2951
+ signatureType: number
2952
+ }
2953
+ accessKeyItems.push({
2954
+ ...itemBase(logIndexOfEvent(event)),
2955
+ data: {
2956
+ account: args.account.toLowerCase() as Address.Address,
2957
+ expiry: Number(args.expiry),
2958
+ publicKey: args.publicKey.toLowerCase() as Address.Address,
2959
+ signatureType: Number(args.signatureType),
2960
+ },
2961
+ type: 'access-key-created',
2962
+ })
2963
+ } else if (
2964
+ event.eventName === 'KeyRevoked' &&
2965
+ Address.isEqual(event.address, Addresses.accountKeychain)
2966
+ ) {
2967
+ const args = event.args as { account: Address.Address; publicKey: Address.Address }
2968
+ accessKeyItems.push({
2969
+ ...itemBase(logIndexOfEvent(event)),
2970
+ data: {
2971
+ account: args.account.toLowerCase() as Address.Address,
2972
+ publicKey: args.publicKey.toLowerCase() as Address.Address,
2973
+ },
2974
+ type: 'access-key-revoked',
2975
+ })
2976
+ }
2977
+ }
2978
+
2979
+ // Token creation.
2980
+ const tokenCreated = finalEvents.find(
2981
+ (e) => e.eventName === 'TokenCreated' && Address.isEqual(e.address, Addresses.tip20Factory),
2982
+ )
2983
+ if (tokenCreated) {
2984
+ const args = tokenCreated.args as {
2985
+ currency: string
2986
+ name: string
2987
+ symbol: string
2988
+ token: Address.Address
2989
+ }
2990
+ return [
2991
+ {
2992
+ ...itemBase(logIndexOfEvent(tokenCreated)),
2993
+ data: {
2994
+ address: args.token.toLowerCase() as Address.Address,
2995
+ currency: args.currency,
2996
+ name: args.name,
2997
+ symbol: args.symbol,
2998
+ },
2999
+ type: 'token-created',
3000
+ },
3001
+ ...accessKeyItems,
3002
+ ]
3003
+ }
3004
+
3005
+ // Governance / admin / DEX-bookkeeping events. Each maps a single decoded
3006
+ // event to a typed item. Checked after token creation (a token-creation tx
3007
+ // also emits `RoleMembershipUpdated`) and before the plain-transfer fallback;
3008
+ // these carry no token movement, so the event itself is the whole signal.
3009
+ const lc = (value: Address.Address) => value.toLowerCase() as Address.Address
3010
+ const hx = (value: Hex.Hex) => value.toLowerCase() as Hex.Hex
3011
+ const findEvent = (name: string) => finalEvents.find((e) => e.eventName === name)
3012
+
3013
+ // DEX bookkeeping (taker swaps and `OrderPlaced` already returned above).
3014
+ const orderFilled = findEvent('OrderFilled')
3015
+ if (orderFilled) {
3016
+ const a = orderFilled.args as {
3017
+ amountFilled: bigint
3018
+ maker: Address.Address
3019
+ orderId: bigint
3020
+ partialFill: boolean
3021
+ taker: Address.Address
3022
+ }
3023
+ return [
3024
+ {
3025
+ ...itemBase(logIndexOfEvent(orderFilled)),
3026
+ data: {
3027
+ amountFilled: a.amountFilled.toString(),
3028
+ maker: lc(a.maker),
3029
+ orderId: a.orderId.toString(),
3030
+ partialFill: a.partialFill,
3031
+ signer,
3032
+ taker: lc(a.taker),
3033
+ },
3034
+ type: 'order-filled',
3035
+ },
3036
+ ]
3037
+ }
3038
+ const orderFlipped = findEvent('OrderFlipped')
3039
+ if (orderFlipped) {
3040
+ const a = orderFlipped.args as {
3041
+ amount: bigint
3042
+ flipTick: number
3043
+ isBid: boolean
3044
+ maker: Address.Address
3045
+ orderId: bigint
3046
+ tick: number
3047
+ token: Address.Address
3048
+ }
3049
+ return [
3050
+ {
3051
+ ...itemBase(logIndexOfEvent(orderFlipped)),
3052
+ data: {
3053
+ flipTick: Number(a.flipTick),
3054
+ maker: lc(a.maker),
3055
+ orderId: a.orderId.toString(),
3056
+ side: a.isBid ? 'bid' : 'ask',
3057
+ signer,
3058
+ sourceToken: token(a.amount, a.token),
3059
+ tick: Number(a.tick),
3060
+ },
3061
+ type: 'order-flipped',
3062
+ },
3063
+ ]
3064
+ }
3065
+ const pairCreated = findEvent('PairCreated')
3066
+ if (pairCreated) {
3067
+ const a = pairCreated.args as { base: Address.Address; key: Hex.Hex; quote: Address.Address }
3068
+ return [
3069
+ {
3070
+ ...itemBase(logIndexOfEvent(pairCreated)),
3071
+ data: { base: lc(a.base), key: hx(a.key), quote: lc(a.quote), signer },
3072
+ type: 'pair-created',
3073
+ },
3074
+ ]
3075
+ }
3076
+
3077
+ // TIP-20 token configuration / admin.
3078
+ const pauseStateUpdate = findEvent('PauseStateUpdate')
3079
+ if (pauseStateUpdate) {
3080
+ const a = pauseStateUpdate.args as { isPaused: boolean; updater: Address.Address }
3081
+ return [
3082
+ {
3083
+ ...itemBase(logIndexOfEvent(pauseStateUpdate)),
3084
+ data: { paused: a.isPaused, signer, updater: lc(a.updater) },
3085
+ type: 'token-pause-set',
3086
+ },
3087
+ ]
3088
+ }
3089
+ const supplyCapUpdate = findEvent('SupplyCapUpdate')
3090
+ if (supplyCapUpdate) {
3091
+ const a = supplyCapUpdate.args as { newSupplyCap: bigint; updater: Address.Address }
3092
+ return [
3093
+ {
3094
+ ...itemBase(logIndexOfEvent(supplyCapUpdate)),
3095
+ data: { signer, supplyCap: a.newSupplyCap.toString(), updater: lc(a.updater) },
3096
+ type: 'token-supply-cap-set',
3097
+ },
3098
+ ]
3099
+ }
3100
+ const transferPolicyUpdate = findEvent('TransferPolicyUpdate')
3101
+ if (transferPolicyUpdate) {
3102
+ const a = transferPolicyUpdate.args as { newPolicyId: bigint; updater: Address.Address }
3103
+ return [
3104
+ {
3105
+ ...itemBase(logIndexOfEvent(transferPolicyUpdate)),
3106
+ data: { policyId: a.newPolicyId.toString(), signer, updater: lc(a.updater) },
3107
+ type: 'token-transfer-policy-set',
3108
+ },
3109
+ ]
3110
+ }
3111
+ const quoteTokenUpdate = findEvent('QuoteTokenUpdate')
3112
+ if (quoteTokenUpdate) {
3113
+ const a = quoteTokenUpdate.args as { newQuoteToken: Address.Address; updater: Address.Address }
3114
+ return [
3115
+ {
3116
+ ...itemBase(logIndexOfEvent(quoteTokenUpdate)),
3117
+ data: { quoteToken: lc(a.newQuoteToken), signer, updater: lc(a.updater) },
3118
+ type: 'token-quote-token-set',
3119
+ },
3120
+ ]
3121
+ }
3122
+ const nextQuoteTokenSet = findEvent('NextQuoteTokenSet')
3123
+ if (nextQuoteTokenSet) {
3124
+ const a = nextQuoteTokenSet.args as {
3125
+ nextQuoteToken: Address.Address
3126
+ updater: Address.Address
3127
+ }
3128
+ return [
3129
+ {
3130
+ ...itemBase(logIndexOfEvent(nextQuoteTokenSet)),
3131
+ data: { nextQuoteToken: lc(a.nextQuoteToken), signer, updater: lc(a.updater) },
3132
+ type: 'token-next-quote-token-set',
3133
+ },
3134
+ ]
3135
+ }
3136
+ const logoUriUpdated = findEvent('LogoURIUpdated')
3137
+ if (logoUriUpdated) {
3138
+ const a = logoUriUpdated.args as { newLogoURI: string; updater: Address.Address }
3139
+ return [
3140
+ {
3141
+ ...itemBase(logIndexOfEvent(logoUriUpdated)),
3142
+ data: { logoUri: a.newLogoURI, signer, updater: lc(a.updater) },
3143
+ type: 'token-logo-set',
3144
+ },
3145
+ ]
3146
+ }
3147
+ const roleMembershipUpdated = findEvent('RoleMembershipUpdated')
3148
+ if (roleMembershipUpdated) {
3149
+ const a = roleMembershipUpdated.args as {
3150
+ account: Address.Address
3151
+ hasRole: boolean
3152
+ role: Hex.Hex
3153
+ sender: Address.Address
3154
+ }
3155
+ return [
3156
+ {
3157
+ ...itemBase(logIndexOfEvent(roleMembershipUpdated)),
3158
+ data: {
3159
+ account: lc(a.account),
3160
+ granted: a.hasRole,
3161
+ role: hx(a.role),
3162
+ sender: lc(a.sender),
3163
+ signer,
3164
+ },
3165
+ type: 'role-membership-set',
3166
+ },
3167
+ ]
3168
+ }
3169
+ const roleAdminUpdated = findEvent('RoleAdminUpdated')
3170
+ if (roleAdminUpdated) {
3171
+ const a = roleAdminUpdated.args as {
3172
+ newAdminRole: Hex.Hex
3173
+ role: Hex.Hex
3174
+ sender: Address.Address
3175
+ }
3176
+ return [
3177
+ {
3178
+ ...itemBase(logIndexOfEvent(roleAdminUpdated)),
3179
+ data: {
3180
+ newAdminRole: hx(a.newAdminRole),
3181
+ role: hx(a.role),
3182
+ sender: lc(a.sender),
3183
+ signer,
3184
+ },
3185
+ type: 'role-admin-set',
3186
+ },
3187
+ ]
3188
+ }
3189
+
3190
+ // Fee-manager token configuration.
3191
+ const userTokenSet = findEvent('UserTokenSet')
3192
+ if (userTokenSet) {
3193
+ const a = userTokenSet.args as { token: Address.Address; user: Address.Address }
3194
+ return [
3195
+ {
3196
+ ...itemBase(logIndexOfEvent(userTokenSet)),
3197
+ data: { signer, token: lc(a.token), user: lc(a.user) },
3198
+ type: 'fee-user-token-set',
3199
+ },
3200
+ ]
3201
+ }
3202
+ const validatorTokenSet = findEvent('ValidatorTokenSet')
3203
+ if (validatorTokenSet) {
3204
+ const a = validatorTokenSet.args as { token: Address.Address; validator: Address.Address }
3205
+ return [
3206
+ {
3207
+ ...itemBase(logIndexOfEvent(validatorTokenSet)),
3208
+ data: { signer, token: lc(a.token), validator: lc(a.validator) },
3209
+ type: 'fee-validator-token-set',
3210
+ },
3211
+ ]
3212
+ }
3213
+
3214
+ // TIP-403 policy registry (create wins over the member events it bundles).
3215
+ const compoundPolicyCreated = findEvent('CompoundPolicyCreated')
3216
+ if (compoundPolicyCreated) {
3217
+ const a = compoundPolicyCreated.args as {
3218
+ creator: Address.Address
3219
+ mintRecipientPolicyId: bigint
3220
+ policyId: bigint
3221
+ recipientPolicyId: bigint
3222
+ senderPolicyId: bigint
3223
+ }
3224
+ return [
3225
+ {
3226
+ ...itemBase(logIndexOfEvent(compoundPolicyCreated)),
3227
+ data: {
3228
+ creator: lc(a.creator),
3229
+ mintRecipientPolicyId: a.mintRecipientPolicyId.toString(),
3230
+ policyId: a.policyId.toString(),
3231
+ recipientPolicyId: a.recipientPolicyId.toString(),
3232
+ senderPolicyId: a.senderPolicyId.toString(),
3233
+ signer,
3234
+ },
3235
+ type: 'compound-policy-created',
3236
+ },
3237
+ ]
3238
+ }
3239
+ const policyCreated = findEvent('PolicyCreated')
3240
+ if (policyCreated) {
3241
+ const a = policyCreated.args as {
3242
+ policyId: bigint
3243
+ policyType: number
3244
+ updater: Address.Address
3245
+ }
3246
+ return [
3247
+ {
3248
+ ...itemBase(logIndexOfEvent(policyCreated)),
3249
+ data: {
3250
+ policyId: a.policyId.toString(),
3251
+ policyType: Number(a.policyType),
3252
+ signer,
3253
+ updater: lc(a.updater),
3254
+ },
3255
+ type: 'policy-created',
3256
+ },
3257
+ ]
3258
+ }
3259
+ const policyAdminUpdated = findEvent('PolicyAdminUpdated')
3260
+ if (policyAdminUpdated) {
3261
+ const a = policyAdminUpdated.args as {
3262
+ admin: Address.Address
3263
+ policyId: bigint
3264
+ updater: Address.Address
3265
+ }
3266
+ return [
3267
+ {
3268
+ ...itemBase(logIndexOfEvent(policyAdminUpdated)),
3269
+ data: {
3270
+ admin: lc(a.admin),
3271
+ policyId: a.policyId.toString(),
3272
+ signer,
3273
+ updater: lc(a.updater),
3274
+ },
3275
+ type: 'policy-admin-set',
3276
+ },
3277
+ ]
3278
+ }
3279
+ const whitelistUpdated = findEvent('WhitelistUpdated')
3280
+ if (whitelistUpdated) {
3281
+ const a = whitelistUpdated.args as {
3282
+ account: Address.Address
3283
+ allowed: boolean
3284
+ policyId: bigint
3285
+ updater: Address.Address
3286
+ }
3287
+ return [
3288
+ {
3289
+ ...itemBase(logIndexOfEvent(whitelistUpdated)),
3290
+ data: {
3291
+ account: lc(a.account),
3292
+ allowed: a.allowed,
3293
+ policyId: a.policyId.toString(),
3294
+ signer,
3295
+ updater: lc(a.updater),
3296
+ },
3297
+ type: 'whitelist-updated',
3298
+ },
3299
+ ]
3300
+ }
3301
+ const blacklistUpdated = findEvent('BlacklistUpdated')
3302
+ if (blacklistUpdated) {
3303
+ const a = blacklistUpdated.args as {
3304
+ account: Address.Address
3305
+ policyId: bigint
3306
+ restricted: boolean
3307
+ updater: Address.Address
3308
+ }
3309
+ return [
3310
+ {
3311
+ ...itemBase(logIndexOfEvent(blacklistUpdated)),
3312
+ data: {
3313
+ account: lc(a.account),
3314
+ policyId: a.policyId.toString(),
3315
+ restricted: a.restricted,
3316
+ signer,
3317
+ updater: lc(a.updater),
3318
+ },
3319
+ type: 'blacklist-updated',
3320
+ },
3321
+ ]
3322
+ }
3323
+
3324
+ // Address / nonce registry.
3325
+ const masterRegistered = findEvent('MasterRegistered')
3326
+ if (masterRegistered) {
3327
+ const a = masterRegistered.args as { masterAddress: Address.Address; masterId: Hex.Hex }
3328
+ return [
3329
+ {
3330
+ ...itemBase(logIndexOfEvent(masterRegistered)),
3331
+ data: { masterAddress: lc(a.masterAddress), masterId: hx(a.masterId), signer },
3332
+ type: 'master-registered',
3333
+ },
3334
+ ]
3335
+ }
3336
+ const nonceIncremented = findEvent('NonceIncremented')
3337
+ if (nonceIncremented) {
3338
+ const a = nonceIncremented.args as {
3339
+ account: Address.Address
3340
+ newNonce: bigint
3341
+ nonceKey: bigint
3342
+ }
3343
+ return [
3344
+ {
3345
+ ...itemBase(logIndexOfEvent(nonceIncremented)),
3346
+ data: {
3347
+ account: lc(a.account),
3348
+ nonce: a.newNonce.toString(),
3349
+ nonceKey: a.nonceKey.toString(),
3350
+ signer,
3351
+ },
3352
+ type: 'nonce-incremented',
3353
+ },
3354
+ ]
3355
+ }
3356
+
3357
+ // Access-key authorization witnesses.
3358
+ const witnessRecorded = findEvent('KeyAuthorizationWitness')
3359
+ if (witnessRecorded) {
3360
+ const a = witnessRecorded.args as { account: Address.Address; witness: Hex.Hex }
3361
+ return [
3362
+ {
3363
+ ...itemBase(logIndexOfEvent(witnessRecorded)),
3364
+ data: { account: lc(a.account), signer, witness: hx(a.witness) },
3365
+ type: 'key-authorization-witness',
3366
+ },
3367
+ ]
3368
+ }
3369
+ const witnessBurned = findEvent('KeyAuthorizationWitnessBurned')
3370
+ if (witnessBurned) {
3371
+ const a = witnessBurned.args as { account: Address.Address; witness: Hex.Hex }
3372
+ return [
3373
+ {
3374
+ ...itemBase(logIndexOfEvent(witnessBurned)),
3375
+ data: { account: lc(a.account), signer, witness: hx(a.witness) },
3376
+ type: 'key-authorization-witness-burned',
3377
+ },
3378
+ ]
3379
+ }
3380
+
3381
+ // Validator configuration (rotation supersedes the add/deactivate it bundles).
3382
+ const validatorRotated = findEvent('ValidatorRotated')
3383
+ if (validatorRotated) {
3384
+ const a = validatorRotated.args as {
3385
+ caller: Address.Address
3386
+ deactivatedIndex: bigint
3387
+ egress: string
3388
+ index: bigint
3389
+ ingress: string
3390
+ newPublicKey: Hex.Hex
3391
+ oldPublicKey: Hex.Hex
3392
+ validatorAddress: Address.Address
3393
+ }
3394
+ return [
3395
+ {
3396
+ ...itemBase(logIndexOfEvent(validatorRotated)),
3397
+ data: {
3398
+ caller: lc(a.caller),
3399
+ deactivatedIndex: a.deactivatedIndex.toString(),
3400
+ egress: a.egress,
3401
+ index: a.index.toString(),
3402
+ ingress: a.ingress,
3403
+ newPublicKey: hx(a.newPublicKey),
3404
+ oldPublicKey: hx(a.oldPublicKey),
3405
+ signer,
3406
+ validator: lc(a.validatorAddress),
3407
+ },
3408
+ type: 'validator-rotated',
3409
+ },
3410
+ ]
3411
+ }
3412
+ const validatorAdded = findEvent('ValidatorAdded')
3413
+ if (validatorAdded) {
3414
+ const a = validatorAdded.args as {
3415
+ egress: string
3416
+ feeRecipient: Address.Address
3417
+ index: bigint
3418
+ ingress: string
3419
+ publicKey: Hex.Hex
3420
+ validatorAddress: Address.Address
3421
+ }
3422
+ return [
3423
+ {
3424
+ ...itemBase(logIndexOfEvent(validatorAdded)),
3425
+ data: {
3426
+ egress: a.egress,
3427
+ feeRecipient: lc(a.feeRecipient),
3428
+ index: a.index.toString(),
3429
+ ingress: a.ingress,
3430
+ publicKey: hx(a.publicKey),
3431
+ signer,
3432
+ validator: lc(a.validatorAddress),
3433
+ },
3434
+ type: 'validator-added',
3435
+ },
3436
+ ]
3437
+ }
3438
+ const validatorDeactivated = findEvent('ValidatorDeactivated')
3439
+ if (validatorDeactivated) {
3440
+ const a = validatorDeactivated.args as { index: bigint; validatorAddress: Address.Address }
3441
+ return [
3442
+ {
3443
+ ...itemBase(logIndexOfEvent(validatorDeactivated)),
3444
+ data: { index: a.index.toString(), signer, validator: lc(a.validatorAddress) },
3445
+ type: 'validator-deactivated',
3446
+ },
3447
+ ]
3448
+ }
3449
+ const validatorMigrated = findEvent('ValidatorMigrated')
3450
+ if (validatorMigrated) {
3451
+ const a = validatorMigrated.args as {
3452
+ index: bigint
3453
+ publicKey: Hex.Hex
3454
+ validatorAddress: Address.Address
3455
+ }
3456
+ return [
3457
+ {
3458
+ ...itemBase(logIndexOfEvent(validatorMigrated)),
3459
+ data: {
3460
+ index: a.index.toString(),
3461
+ publicKey: hx(a.publicKey),
3462
+ signer,
3463
+ validator: lc(a.validatorAddress),
3464
+ },
3465
+ type: 'validator-migrated',
3466
+ },
3467
+ ]
3468
+ }
3469
+ const validatorMigrationSkipped = findEvent('SkippedValidatorMigration')
3470
+ if (validatorMigrationSkipped) {
3471
+ const a = validatorMigrationSkipped.args as {
3472
+ index: bigint
3473
+ publicKey: Hex.Hex
3474
+ validatorAddress: Address.Address
3475
+ }
3476
+ return [
3477
+ {
3478
+ ...itemBase(logIndexOfEvent(validatorMigrationSkipped)),
3479
+ data: {
3480
+ index: a.index.toString(),
3481
+ publicKey: hx(a.publicKey),
3482
+ signer,
3483
+ validator: lc(a.validatorAddress),
3484
+ },
3485
+ type: 'validator-migration-skipped',
3486
+ },
3487
+ ]
3488
+ }
3489
+ const feeRecipientUpdated = findEvent('FeeRecipientUpdated')
3490
+ if (feeRecipientUpdated) {
3491
+ const a = feeRecipientUpdated.args as {
3492
+ caller: Address.Address
3493
+ feeRecipient: Address.Address
3494
+ index: bigint
3495
+ }
3496
+ return [
3497
+ {
3498
+ ...itemBase(logIndexOfEvent(feeRecipientUpdated)),
3499
+ data: {
3500
+ caller: lc(a.caller),
3501
+ feeRecipient: lc(a.feeRecipient),
3502
+ index: a.index.toString(),
3503
+ signer,
3504
+ },
3505
+ type: 'validator-fee-recipient-set',
3506
+ },
3507
+ ]
3508
+ }
3509
+ const ipAddressesUpdated = findEvent('IpAddressesUpdated')
3510
+ if (ipAddressesUpdated) {
3511
+ const a = ipAddressesUpdated.args as {
3512
+ caller: Address.Address
3513
+ egress: string
3514
+ index: bigint
3515
+ ingress: string
3516
+ }
3517
+ return [
3518
+ {
3519
+ ...itemBase(logIndexOfEvent(ipAddressesUpdated)),
3520
+ data: {
3521
+ caller: lc(a.caller),
3522
+ egress: a.egress,
3523
+ index: a.index.toString(),
3524
+ ingress: a.ingress,
3525
+ signer,
3526
+ },
3527
+ type: 'validator-ip-set',
3528
+ },
3529
+ ]
3530
+ }
3531
+ const validatorOwnershipTransferred = findEvent('ValidatorOwnershipTransferred')
3532
+ if (validatorOwnershipTransferred) {
3533
+ const a = validatorOwnershipTransferred.args as {
3534
+ caller: Address.Address
3535
+ index: bigint
3536
+ newAddress: Address.Address
3537
+ oldAddress: Address.Address
3538
+ }
3539
+ return [
3540
+ {
3541
+ ...itemBase(logIndexOfEvent(validatorOwnershipTransferred)),
3542
+ data: {
3543
+ caller: lc(a.caller),
3544
+ index: a.index.toString(),
3545
+ newAddress: lc(a.newAddress),
3546
+ oldAddress: lc(a.oldAddress),
3547
+ signer,
3548
+ },
3549
+ type: 'validator-ownership-transferred',
3550
+ },
3551
+ ]
3552
+ }
3553
+ const ownershipTransferred = findEvent('OwnershipTransferred')
3554
+ if (ownershipTransferred) {
3555
+ const a = ownershipTransferred.args as {
3556
+ newOwner: Address.Address
3557
+ oldOwner: Address.Address
3558
+ }
3559
+ return [
3560
+ {
3561
+ ...itemBase(logIndexOfEvent(ownershipTransferred)),
3562
+ data: { newOwner: lc(a.newOwner), oldOwner: lc(a.oldOwner), signer },
3563
+ type: 'ownership-transferred',
3564
+ },
3565
+ ]
3566
+ }
3567
+ const rotationEpochSet = findEvent('NetworkIdentityRotationEpochSet')
3568
+ if (rotationEpochSet) {
3569
+ const a = rotationEpochSet.args as { nextEpoch: bigint; previousEpoch: bigint }
3570
+ return [
3571
+ {
3572
+ ...itemBase(logIndexOfEvent(rotationEpochSet)),
3573
+ data: {
3574
+ nextEpoch: a.nextEpoch.toString(),
3575
+ previousEpoch: a.previousEpoch.toString(),
3576
+ signer,
3577
+ },
3578
+ type: 'network-identity-rotation-epoch-set',
3579
+ },
3580
+ ]
3581
+ }
3582
+ const initialized = findEvent('Initialized')
3583
+ if (initialized) {
3584
+ const a = initialized.args as { height: bigint }
3585
+ return [
3586
+ {
3587
+ ...itemBase(logIndexOfEvent(initialized)),
3588
+ data: { height: a.height.toString(), signer },
3589
+ type: 'initialized',
3590
+ },
3591
+ ]
3592
+ }
3593
+
3594
+ // Default: plain transfer (excluding transfers to the fee manager).
3595
+ const userTransfers = transfers.filter(
3596
+ (e) => !Address.isEqual((e.args as { to: Address.Address }).to, Addresses.feeManager),
3597
+ )
3598
+ if (userTransfers.length > 0) {
3599
+ const t = userTransfers[0]!
3600
+ const args = t.args as {
3601
+ amount: bigint
3602
+ from: Address.Address
3603
+ memo?: Hex.Hex | undefined
3604
+ to: Address.Address
3605
+ }
3606
+ const to = args.to.toLowerCase() as Address.Address
3607
+ const from = args.from.toLowerCase() as Address.Address
3608
+ const rawMemo = t.eventName === 'TransferWithMemo' ? args.memo : undefined
3609
+ // The raw memo (not the suppressed display string) carries the MPP
3610
+ // attribution fingerprint, so resolve the service name from it.
3611
+ const memo = rawMemo ? Transfers.memoToString(rawMemo) : undefined
3612
+ const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo })
3613
+ const sourceToken = token(args.amount, t.address as Address.Address)
3614
+ if (Address.isEqual(to, viewer))
3615
+ return [
3616
+ transfer(itemBase(logIndexOfEvent(t)), blockNum, {
3617
+ attribution,
3618
+ direction: 'in',
3619
+ memo,
3620
+ recipient: viewer,
3621
+ sender: from,
3622
+ sourceToken,
3623
+ }),
3624
+ ...accessKeyItems,
3625
+ ]
3626
+ return [
3627
+ transfer(itemBase(logIndexOfEvent(t)), blockNum, {
3628
+ attribution,
3629
+ direction: 'out',
3630
+ memo,
3631
+ recipient: to,
3632
+ sender: from,
3633
+ signer,
3634
+ sourceToken,
3635
+ }),
3636
+ ...accessKeyItems,
3637
+ ]
3638
+ }
3639
+
3640
+ // Pure access-key tx (no transfer / mint / burn / etc.).
3641
+ if (accessKeyItems.length > 0) return accessKeyItems
3642
+
3643
+ // Unclassified: a tx the address sent or paid for whose logs matched no known
3644
+ // classifier. Surface it honestly as an `unknown` item carrying every log
3645
+ // (decoded when possible), rather than fabricating a zero transfer.
3646
+ return [unknown(itemBase(minLogIndex), { events: buildTxEvents(rows, events), signer })]
3647
+ }
3648
+
3649
+ /** Extracts the block-wide log index from a decoded viem event. */
3650
+ function logIndexOfEvent(event: { logIndex: number }): number {
3651
+ return event.logIndex
3652
+ }
3653
+
3654
+ /**
3655
+ * Builds a `transfer` item whose `data` is the `Transfer` API resource plus the
3656
+ * feed-only `direction`/`memo`/`attribution`/`signer` fields. Optional fields
3657
+ * are omitted when absent (`exactOptionalPropertyTypes`).
3658
+ */
3659
+ function transfer(
3660
+ base: Base,
3661
+ blockNumber: number,
3662
+ data: {
3663
+ attribution?: string | undefined
3664
+ destinationToken?: Token | undefined
3665
+ direction: 'in' | 'out'
3666
+ memo?: string | undefined
3667
+ recipient: Address.Address
3668
+ sender: Address.Address
3669
+ signer?: 'self' | Address.Address | undefined
3670
+ sourceToken: Token
3671
+ },
3672
+ ): Extract<RawItem, { type: 'transfer' }> {
3673
+ return {
3674
+ ...base,
3675
+ data: {
3676
+ ...(data.attribution ? { attribution: data.attribution } : {}),
3677
+ blockNumber,
3678
+ ...(data.destinationToken ? { destinationToken: data.destinationToken } : {}),
3679
+ direction: data.direction,
3680
+ ...(data.memo ? { memo: data.memo } : {}),
3681
+ recipient: data.recipient,
3682
+ sender: data.sender,
3683
+ ...(data.signer ? { signer: data.signer } : {}),
3684
+ sourceToken: data.sourceToken,
3685
+ timestamp: base.timestamp,
3686
+ transactionHash: base.transactionHash,
3687
+ },
3688
+ type: 'transfer',
3689
+ }
3690
+ }
3691
+
3692
+ /** One entry in an item's `events` capture: a log, decoded when possible (derived from the schema). */
3693
+ type TxEvent = NonNullable<Item['events']>[number]
3694
+
3695
+ /** The structural slice of a decoded `parseEventLogs` event that {@link buildTxEvents} reads. */
3696
+ type DecodedEvent = { args: unknown; eventName: string; logIndex: number }
3697
+
3698
+ /**
3699
+ * Builds an `unknown` item for a transaction the address sent or paid for that
3700
+ * did not match any known classifier. Carries the real `transactionHash`/
3701
+ * `timestamp` plus every log of the transaction in the top-level `events`
3702
+ * array (decoded into `eventName`/`args`/`title` when the log matched a known
3703
+ * `viem/tempo` ABI, otherwise just the raw `topics`/`data`), so no log is ever
3704
+ * silently dropped.
3705
+ */
3706
+ function unknown(
3707
+ base: Base,
3708
+ data: { events: TxEvent[]; signer: 'self' | Address.Address },
3709
+ ): Extract<RawItem, { type: 'unknown' }> {
3710
+ return { ...base, data: { signer: data.signer }, events: data.events, type: 'unknown' }
3711
+ }
3712
+
3713
+ /** Decodes a group's raw rows into `viem/tempo` events via `parseEventLogs` (non-strict). */
3714
+ function decodeRows(rows: readonly Row[]) {
3715
+ const viemLogs: Log[] = rows.map((r) => ({
3716
+ address: r.logAddress as `0x${string}`,
3717
+ blockHash: '0x0' as Hex.Hex,
3718
+ blockNumber: BigInt(r.blockNum),
3719
+ data: (r.data ?? '0x') as Hex.Hex,
3720
+ logIndex: r.logIdx,
3721
+ removed: false,
3722
+ topics: [r.selector, r.topic1, r.topic2, r.topic3].filter(Boolean) as [Hex.Hex, ...Hex.Hex[]],
3723
+ transactionHash: r.txHash as `0x${string}`,
3724
+ transactionIndex: r.txIdx,
3725
+ }))
3726
+ return parseEventLogs({ abi: [...Abis.abis], logs: viemLogs, strict: false })
3727
+ }
3728
+
3729
+ /**
3730
+ * Builds the full per-transaction `events` capture: one entry per raw log,
3731
+ * enriched with `eventName`/`args`/`title` when that log decoded into a known
3732
+ * event. Decoded entries are matched to their raw log by block-wide log index,
3733
+ * so the result covers every log (decoded or not) exactly once.
3734
+ */
3735
+ function buildTxEvents(rows: readonly Row[], decoded: readonly DecodedEvent[]): TxEvent[] {
3736
+ const decodedByIndex = new Map(decoded.map((event) => [event.logIndex, event]))
3737
+ return rows
3738
+ .map((row) => {
3739
+ const base = {
3740
+ address: row.logAddress,
3741
+ data: (row.data ?? '0x') as Hex.Hex,
3742
+ logIndex: row.logIdx,
3743
+ topics: [row.selector, row.topic1, row.topic2, row.topic3].filter(
3744
+ (topic): topic is Hex.Hex => topic !== null,
3745
+ ),
3746
+ }
3747
+ const event = decodedByIndex.get(row.logIdx)
3748
+ if (!event) return base
3749
+ return {
3750
+ ...base,
3751
+ args: (jsonSafe(event.args) ?? {}) as Record<string, unknown>,
3752
+ eventName: event.eventName,
3753
+ ...(titleByEventName[event.eventName] ? { title: titleByEventName[event.eventName] } : {}),
3754
+ }
3755
+ })
3756
+ .sort((a, b) => a.logIndex - b.logIndex)
3757
+ }
3758
+
3759
+ /**
3760
+ * Recursively converts decoded-event values into JSON-serializable form: bigints
3761
+ * become decimal strings, address-shaped strings are lowercased to match the
3762
+ * API's address convention (viem returns them checksummed), arrays/objects are
3763
+ * walked, everything else passes through. Applied to `parseEventLogs` args
3764
+ * before they cross the API boundary.
3765
+ */
3766
+ function jsonSafe(value: unknown): unknown {
3767
+ if (typeof value === 'bigint') return value.toString()
3768
+ if (typeof value === 'string')
3769
+ return /^0x[0-9a-fA-F]{40}$/.test(value) ? value.toLowerCase() : value
3770
+ if (Array.isArray(value)) return value.map(jsonSafe)
3771
+ if (value && typeof value === 'object') {
3772
+ const out: Record<string, unknown> = {}
3773
+ for (const [key, val] of Object.entries(value)) out[key] = jsonSafe(val)
3774
+ return out
3775
+ }
3776
+ return value
3777
+ }
3778
+
3779
+ /** Lowercases a TIDX address column, defaulting to the zero address. */
3780
+ function lower(value: unknown): Address.Address {
3781
+ return (Value.toText(value)?.toLowerCase() ?? zeroAddress) as Address.Address
3782
+ }
3783
+
3784
+ /** Lowercases a TIDX transaction hash column, defaulting to `0x` when absent. */
3785
+ function lowerHash(value: unknown): Hex.Hex {
3786
+ return (Value.toText(value)?.toLowerCase() ?? '0x') as Hex.Hex
3787
+ }
3788
+
3789
+ /** Lowercases an optional TIDX address column, or null when absent. */
3790
+ function lowerOrNull(value: unknown): Address.Address | null {
3791
+ const text = Value.toText(value)
3792
+ return text ? (text.toLowerCase() as Address.Address) : null
3793
+ }
3794
+
3795
+ /** Builds a stub token; `decimals`/`symbol` are filled by {@link enrichItem}. */
3796
+ function token(amount: bigint | undefined, address: Address.Address): Token {
3797
+ return { address: address.toLowerCase() as Address.Address, amount: (amount ?? 0n).toString() }
3798
+ }
3799
+
3800
+ /** Collects every token address referenced by an item's `data`. */
3801
+ function collectTokens(item: Item, set: Set<Address.Address>) {
3802
+ const data = item.data as Partial<Record<(typeof tokenKeys)[number], Token>>
3803
+ for (const key of tokenKeys) {
3804
+ const t = data[key]
3805
+ if (t) set.add(t.address.toLowerCase() as Address.Address)
3806
+ }
3807
+ }
3808
+
3809
+ /** Resolved token metadata keyed by lowercased address. */
3810
+ type ResolvedToken = Awaited<ReturnType<typeof Tokens.getTokenMetadata>>
3811
+
3812
+ /**
3813
+ * Resolves metadata for every token referenced across a page of items and
3814
+ * spreads it into each item's stub tokens, in place. Routes through the shared
3815
+ * {@link Tokens.getTokenMetadata} (one `token.getMetadata` RPC per token,
3816
+ * memoized under the `token:v1:…` key shared with `/tokens` and `/transfers`)
3817
+ * instead of a parallel metadata fetch. Resolution is best-effort: a token
3818
+ * whose metadata is unavailable keeps just `address`/`amount`.
3819
+ */
3820
+ async function enrichTokens(c: Context<App.Environment>, options: enrichTokens.Options) {
3821
+ const { chainId, items } = options
3822
+ const addresses = new Set<Address.Address>()
3823
+ for (const item of items) collectTokens(item, addresses)
3824
+ if (addresses.size === 0) return
3825
+
3826
+ const resolved = await Timing.time(c, 'tokens', () =>
3827
+ Promise.all(
3828
+ [...addresses].map((address) =>
3829
+ Tokens.getTokenMetadata(c, { address, chainId })
3830
+ .then((metadata) => [address.toLowerCase(), metadata] as const)
3831
+ .catch(() => [address.toLowerCase(), undefined] as const),
3832
+ ),
3833
+ ),
3834
+ )
3835
+ const byAddress = new Map(resolved)
3836
+ for (const item of items) enrichItem(item, byAddress)
3837
+ }
3838
+
3839
+ declare namespace enrichTokens {
3840
+ type Options = {
3841
+ chainId: z.output<typeof Schema.ChainId>
3842
+ items: readonly Item[]
3843
+ }
3844
+ }
3845
+
3846
+ /** Spreads resolved token metadata into an item's stub tokens, in place. */
3847
+ function enrichItem(item: Item, byAddress: Map<string, ResolvedToken | undefined>) {
3848
+ const data = item.data as Partial<Record<(typeof tokenKeys)[number], Token>>
3849
+ for (const key of tokenKeys) {
3850
+ const t = data[key]
3851
+ if (!t) continue
3852
+ const meta = byAddress.get(t.address.toLowerCase())
3853
+ if (!meta) continue
3854
+ // Carry only the per-row token fields the `Token` (SourceToken) shape
3855
+ // declares; `address`/`amount` come from the stub.
3856
+ data[key] = {
3857
+ ...t,
3858
+ currency: meta.currency,
3859
+ decimals: meta.decimals,
3860
+ name: meta.name,
3861
+ symbol: meta.symbol,
3862
+ }
3863
+ }
3864
+ }
3865
+
3866
+ /**
3867
+ * Folds adjacent items signed by the same access key on the same UTC calendar
3868
+ * day into a single `group` entry. Self-signed items pass through untouched.
3869
+ * Must be called once over the full, timestamp-sorted item list.
3870
+ */
3871
+ export function groupByAccessKey(items: Item[]): Entry[] {
3872
+ const entries: Entry[] = []
3873
+ let currentGroup: { items: Item[]; signer: Address.Address } | null = null
3874
+
3875
+ for (const item of items) {
3876
+ const itemSigner = signerOf(item)
3877
+ if (itemSigner !== 'self') {
3878
+ if (
3879
+ currentGroup &&
3880
+ Address.isEqual(currentGroup.signer, itemSigner) &&
3881
+ sameCalendarDay(currentGroup.items[0]!.timestamp, item.timestamp)
3882
+ ) {
3883
+ currentGroup.items.push(item)
3884
+ if (currentGroup.items.length >= maxGroupSize) {
3885
+ entries.push(flushGroup(currentGroup))
3886
+ currentGroup = null
3887
+ }
3888
+ } else {
3889
+ if (currentGroup) entries.push(flushGroup(currentGroup))
3890
+ currentGroup = { items: [item], signer: itemSigner }
3891
+ }
3892
+ } else {
3893
+ if (currentGroup) {
3894
+ entries.push(flushGroup(currentGroup))
3895
+ currentGroup = null
3896
+ }
3897
+ entries.push(item)
3898
+ }
3899
+ }
3900
+
3901
+ if (currentGroup) entries.push(flushGroup(currentGroup))
3902
+ return entries
3903
+ }
3904
+
3905
+ /** The access-key signer of an item, or `self` when it has no access-key signer. */
3906
+ function signerOf(item: Item): 'self' | Address.Address {
3907
+ return (item.data as { signer?: 'self' | Address.Address }).signer ?? 'self'
3908
+ }
3909
+
3910
+ /** Unwraps single-item groups; otherwise wraps the items in a `group` entry. */
3911
+ function flushGroup(group: { items: Item[]; signer: Address.Address }): Entry {
3912
+ if (group.items.length === 1) return group.items[0]!
3913
+ const firstItem = group.items[0]!
3914
+ const lastItem = group.items[group.items.length - 1]!
3915
+ return {
3916
+ data: { items: group.items, signer: group.signer },
3917
+ perspective: firstItem.perspective,
3918
+ id: `${firstItem.id}..${lastItem.id}:${group.items.length}`,
3919
+ timestamp: firstItem.timestamp,
3920
+ title: 'Group',
3921
+ type: 'group',
3922
+ }
3923
+ }
3924
+
3925
+ /** Whether two ISO 8601 timestamps fall on the same UTC calendar day. */
3926
+ function sameCalendarDay(a: string, b: string): boolean {
3927
+ const da = new Date(a)
3928
+ const db = new Date(b)
3929
+ return (
3930
+ da.getUTCFullYear() === db.getUTCFullYear() &&
3931
+ da.getUTCMonth() === db.getUTCMonth() &&
3932
+ da.getUTCDate() === db.getUTCDate()
3933
+ )
3934
+ }