tapimo 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/dist/App.d.ts +6396 -4575
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +39 -4
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.d.ts +15 -0
  6. package/dist/Assets.d.ts.map +1 -1
  7. package/dist/Assets.js +7 -0
  8. package/dist/Assets.js.map +1 -1
  9. package/dist/Scope.d.ts +2 -0
  10. package/dist/Scope.d.ts.map +1 -1
  11. package/dist/Scope.js +17 -0
  12. package/dist/Scope.js.map +1 -1
  13. package/dist/admin/App.d.ts +176 -78
  14. package/dist/admin/App.d.ts.map +1 -1
  15. package/dist/admin/apps/api-keys.d.ts +84 -4
  16. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.js +54 -0
  18. package/dist/admin/apps/api-keys.js.map +1 -1
  19. package/dist/admin/apps/earn-vaults.d.ts +123 -9
  20. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  21. package/dist/admin/apps/earn-vaults.js +60 -7
  22. package/dist/admin/apps/earn-vaults.js.map +1 -1
  23. package/dist/admin/apps/organizations.d.ts.map +1 -1
  24. package/dist/admin/apps/organizations.js +4 -2
  25. package/dist/admin/apps/organizations.js.map +1 -1
  26. package/dist/admin/apps/verified-tokens.d.ts +90 -90
  27. package/dist/admin/ui.generated.d.ts +1 -1
  28. package/dist/admin/ui.generated.js +71 -71
  29. package/dist/admin/ui.generated.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +16 -16
  31. package/dist/apps/Zones.js +4 -4
  32. package/dist/apps/data/App.d.ts +1699 -749
  33. package/dist/apps/data/App.d.ts.map +1 -1
  34. package/dist/apps/data/App.js +58 -9
  35. package/dist/apps/data/App.js.map +1 -1
  36. package/dist/apps/data/ZoneSelection.d.ts +62 -0
  37. package/dist/apps/data/ZoneSelection.d.ts.map +1 -0
  38. package/dist/apps/data/ZoneSelection.js +44 -0
  39. package/dist/apps/data/ZoneSelection.js.map +1 -0
  40. package/dist/apps/data/routes/activities.d.ts +2417 -50
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +1896 -1293
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +471 -340
  45. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.d.ts +12 -12
  47. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  48. package/dist/apps/data/routes/balances.js +44 -17
  49. package/dist/apps/data/routes/balances.js.map +1 -1
  50. package/dist/apps/data/routes/blocks.d.ts +42 -42
  51. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  52. package/dist/apps/data/routes/blocks.js +22 -12
  53. package/dist/apps/data/routes/blocks.js.map +1 -1
  54. package/dist/apps/data/routes/coingecko.d.ts +179 -179
  55. package/dist/apps/data/routes/earn.d.ts +144 -19
  56. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  57. package/dist/apps/data/routes/earn.js +160 -28
  58. package/dist/apps/data/routes/earn.js.map +1 -1
  59. package/dist/apps/data/routes/exchanges.d.ts +719 -13
  60. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  61. package/dist/apps/data/routes/exchanges.js +16 -13
  62. package/dist/apps/data/routes/exchanges.js.map +1 -1
  63. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  64. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  65. package/dist/apps/data/routes/fee-amm.js +9 -11
  66. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  67. package/dist/apps/data/routes/indexer.d.ts +34 -34
  68. package/dist/apps/data/routes/receipts.d.ts +363 -263
  69. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  70. package/dist/apps/data/routes/receipts.js +29 -11
  71. package/dist/apps/data/routes/receipts.js.map +1 -1
  72. package/dist/apps/data/routes/rpc.js +24 -10
  73. package/dist/apps/data/routes/rpc.js.map +1 -1
  74. package/dist/apps/data/routes/tokenlist.d.ts +13 -13
  75. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  76. package/dist/apps/data/routes/tokenlist.js +16 -15
  77. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  78. package/dist/apps/data/routes/tokens.d.ts +112 -98
  79. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  80. package/dist/apps/data/routes/tokens.js +85 -35
  81. package/dist/apps/data/routes/tokens.js.map +1 -1
  82. package/dist/apps/data/routes/transactions.d.ts +7 -2
  83. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  84. package/dist/apps/data/routes/transactions.js +38 -18
  85. package/dist/apps/data/routes/transactions.js.map +1 -1
  86. package/dist/apps/data/routes/transfers.d.ts +1 -1
  87. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  88. package/dist/apps/data/routes/transfers.js +25 -21
  89. package/dist/apps/data/routes/transfers.js.map +1 -1
  90. package/dist/apps/data/routes/valuation.d.ts +1 -1
  91. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  92. package/dist/apps/data/routes/valuation.js +12 -7
  93. package/dist/apps/data/routes/valuation.js.map +1 -1
  94. package/dist/apps/data/routes/verified-tokens.d.ts +3 -3
  95. package/dist/apps/data/routes/webhooks.d.ts +15 -15
  96. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  97. package/dist/apps/data/routes/webhooks.js +2 -2
  98. package/dist/apps/data/routes/webhooks.js.map +1 -1
  99. package/dist/apps/data/routes/zones.d.ts +32 -32
  100. package/dist/apps/data/routes/zones.js +1 -1
  101. package/dist/apps/funding/App.d.ts +38 -27
  102. package/dist/apps/funding/App.d.ts.map +1 -1
  103. package/dist/apps/funding/routes/chains.d.ts.map +1 -1
  104. package/dist/apps/funding/routes/chains.js +5 -4
  105. package/dist/apps/funding/routes/chains.js.map +1 -1
  106. package/dist/apps/funding/routes/deposit-addresses.d.ts +16 -16
  107. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  108. package/dist/apps/funding/routes/deposit-addresses.js +24 -19
  109. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  110. package/dist/apps/funding/routes/providers.d.ts +13 -13
  111. package/dist/apps/funding/routes/providers.d.ts.map +1 -1
  112. package/dist/apps/funding/routes/providers.js +3 -2
  113. package/dist/apps/funding/routes/providers.js.map +1 -1
  114. package/dist/apps/funding/routes/quotes.d.ts.map +1 -1
  115. package/dist/apps/funding/routes/quotes.js +2 -2
  116. package/dist/apps/funding/routes/quotes.js.map +1 -1
  117. package/dist/apps/funding/routes/transfers.d.ts +60 -48
  118. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  119. package/dist/apps/funding/routes/transfers.js +27 -19
  120. package/dist/apps/funding/routes/transfers.js.map +1 -1
  121. package/dist/apps/management/App.d.ts +4558 -3700
  122. package/dist/apps/management/App.d.ts.map +1 -1
  123. package/dist/apps/management/Billing.d.ts.map +1 -1
  124. package/dist/apps/management/Billing.js +19 -6
  125. package/dist/apps/management/Billing.js.map +1 -1
  126. package/dist/apps/management/routes/api-keys.d.ts +786 -175
  127. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  128. package/dist/apps/management/routes/api-keys.js +312 -34
  129. package/dist/apps/management/routes/api-keys.js.map +1 -1
  130. package/dist/apps/management/routes/billing.d.ts +103 -30
  131. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  132. package/dist/apps/management/routes/billing.js +91 -32
  133. package/dist/apps/management/routes/billing.js.map +1 -1
  134. package/dist/apps/management/routes/faucet.d.ts +35 -35
  135. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  136. package/dist/apps/management/routes/faucet.js +23 -12
  137. package/dist/apps/management/routes/faucet.js.map +1 -1
  138. package/dist/apps/management/routes/invitations.d.ts +46 -13
  139. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  140. package/dist/apps/management/routes/invitations.js +79 -29
  141. package/dist/apps/management/routes/invitations.js.map +1 -1
  142. package/dist/apps/management/routes/invite-links.d.ts +120 -75
  143. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  144. package/dist/apps/management/routes/invite-links.js +100 -41
  145. package/dist/apps/management/routes/invite-links.js.map +1 -1
  146. package/dist/apps/management/routes/me.d.ts +5 -5
  147. package/dist/apps/management/routes/me.d.ts.map +1 -1
  148. package/dist/apps/management/routes/me.js +14 -10
  149. package/dist/apps/management/routes/me.js.map +1 -1
  150. package/dist/apps/management/routes/members.d.ts +99 -78
  151. package/dist/apps/management/routes/members.d.ts.map +1 -1
  152. package/dist/apps/management/routes/members.js +37 -18
  153. package/dist/apps/management/routes/members.js.map +1 -1
  154. package/dist/apps/management/routes/orgs.d.ts +48 -8
  155. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  156. package/dist/apps/management/routes/orgs.js +48 -19
  157. package/dist/apps/management/routes/orgs.js.map +1 -1
  158. package/dist/apps/management/routes/projects.d.ts +108 -46
  159. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  160. package/dist/apps/management/routes/projects.js +72 -21
  161. package/dist/apps/management/routes/projects.js.map +1 -1
  162. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  163. package/dist/apps/management/routes/scopes.js +8 -5
  164. package/dist/apps/management/routes/scopes.js.map +1 -1
  165. package/dist/apps/management/routes/usage.d.ts +82 -82
  166. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  167. package/dist/apps/management/routes/usage.js +41 -31
  168. package/dist/apps/management/routes/usage.js.map +1 -1
  169. package/dist/apps/management/routes/verified-token-requests.d.ts +35 -7
  170. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -1
  171. package/dist/apps/management/routes/verified-token-requests.js +36 -17
  172. package/dist/apps/management/routes/verified-token-requests.js.map +1 -1
  173. package/dist/apps/management/routes/webhooks.d.ts +200 -183
  174. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  175. package/dist/apps/management/routes/webhooks.js +80 -35
  176. package/dist/apps/management/routes/webhooks.js.map +1 -1
  177. package/dist/apps/mpp/internal/Adapter.d.ts +1 -1
  178. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  179. package/dist/apps/mpp/internal/Adapter.js +12 -0
  180. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  181. package/dist/db/Db.d.ts +2 -0
  182. package/dist/db/Db.d.ts.map +1 -1
  183. package/dist/db/Db.js +52 -1
  184. package/dist/db/Db.js.map +1 -1
  185. package/dist/db/Schema.d.ts +26 -3
  186. package/dist/db/Schema.d.ts.map +1 -1
  187. package/dist/db/Schema.js +24 -5
  188. package/dist/db/Schema.js.map +1 -1
  189. package/dist/db/tables/earnVaults.d.ts +5 -1
  190. package/dist/db/tables/earnVaults.d.ts.map +1 -1
  191. package/dist/db/tables/earnVaults.js +5 -0
  192. package/dist/db/tables/earnVaults.js.map +1 -1
  193. package/dist/db/tables/fundingCatalog.d.ts +4 -2
  194. package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
  195. package/dist/db/tables/fundingCatalog.js +5 -1
  196. package/dist/db/tables/fundingCatalog.js.map +1 -1
  197. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  198. package/dist/db/tables/webhookDeliveries.js +10 -0
  199. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  200. package/dist/db/tables/webhookQueueCompletions.d.ts +15 -0
  201. package/dist/db/tables/webhookQueueCompletions.d.ts.map +1 -0
  202. package/dist/db/tables/webhookQueueCompletions.js +32 -0
  203. package/dist/db/tables/webhookQueueCompletions.js.map +1 -0
  204. package/dist/db/tables/webhookQueueEvents.d.ts +14 -20
  205. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
  206. package/dist/db/tables/webhookQueueEvents.js +68 -47
  207. package/dist/db/tables/webhookQueueEvents.js.map +1 -1
  208. package/dist/db/tables/webhookSubscriptions.d.ts +16 -5
  209. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  210. package/dist/db/tables/webhookSubscriptions.js +29 -5
  211. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  212. package/dist/handlers/internal/utils.d.ts.map +1 -1
  213. package/dist/handlers/internal/utils.js +9 -0
  214. package/dist/handlers/internal/utils.js.map +1 -1
  215. package/dist/internal/Auth.d.ts +11 -2
  216. package/dist/internal/Auth.d.ts.map +1 -1
  217. package/dist/internal/Auth.js +19 -6
  218. package/dist/internal/Auth.js.map +1 -1
  219. package/dist/internal/Cache.d.ts.map +1 -1
  220. package/dist/internal/Cache.js +11 -7
  221. package/dist/internal/Cache.js.map +1 -1
  222. package/dist/internal/EarnVaults.d.ts +43 -4
  223. package/dist/internal/EarnVaults.d.ts.map +1 -1
  224. package/dist/internal/EarnVaults.js +167 -0
  225. package/dist/internal/EarnVaults.js.map +1 -1
  226. package/dist/internal/Fetch.d.ts +23 -0
  227. package/dist/internal/Fetch.d.ts.map +1 -0
  228. package/dist/internal/Fetch.js +26 -0
  229. package/dist/internal/Fetch.js.map +1 -0
  230. package/dist/internal/Log.d.ts +3 -1
  231. package/dist/internal/Log.d.ts.map +1 -1
  232. package/dist/internal/Log.js +16 -3
  233. package/dist/internal/Log.js.map +1 -1
  234. package/dist/internal/OpenApi.d.ts +30 -24
  235. package/dist/internal/OpenApi.d.ts.map +1 -1
  236. package/dist/internal/OpenApi.js +140 -41
  237. package/dist/internal/OpenApi.js.map +1 -1
  238. package/dist/internal/Response.d.ts +14 -9
  239. package/dist/internal/Response.d.ts.map +1 -1
  240. package/dist/internal/Response.js +16 -5
  241. package/dist/internal/Response.js.map +1 -1
  242. package/dist/internal/Schema.d.ts +11 -10
  243. package/dist/internal/Schema.d.ts.map +1 -1
  244. package/dist/internal/Schema.js +14 -9
  245. package/dist/internal/Schema.js.map +1 -1
  246. package/dist/internal/Store.d.ts +28 -6
  247. package/dist/internal/Store.d.ts.map +1 -1
  248. package/dist/internal/Store.js +121 -25
  249. package/dist/internal/Store.js.map +1 -1
  250. package/dist/internal/Tidx.d.ts +2 -0
  251. package/dist/internal/Tidx.d.ts.map +1 -1
  252. package/dist/internal/Tidx.js +23 -6
  253. package/dist/internal/Tidx.js.map +1 -1
  254. package/dist/internal/Timeout.d.ts +36 -0
  255. package/dist/internal/Timeout.d.ts.map +1 -0
  256. package/dist/internal/Timeout.js +37 -0
  257. package/dist/internal/Timeout.js.map +1 -0
  258. package/dist/internal/Timing.d.ts +9 -0
  259. package/dist/internal/Timing.d.ts.map +1 -1
  260. package/dist/internal/Timing.js +7 -0
  261. package/dist/internal/Timing.js.map +1 -1
  262. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  263. package/dist/internal/WebhookDestination.js +3 -0
  264. package/dist/internal/WebhookDestination.js.map +1 -1
  265. package/dist/internal/Webhooks.d.ts +29 -12
  266. package/dist/internal/Webhooks.d.ts.map +1 -1
  267. package/dist/internal/Webhooks.js +37 -20
  268. package/dist/internal/Webhooks.js.map +1 -1
  269. package/dist/internal/funding/Action.d.ts +9 -0
  270. package/dist/internal/funding/Action.d.ts.map +1 -1
  271. package/dist/internal/funding/Action.js +16 -8
  272. package/dist/internal/funding/Action.js.map +1 -1
  273. package/dist/internal/funding/Catalog.d.ts +9 -2
  274. package/dist/internal/funding/Catalog.d.ts.map +1 -1
  275. package/dist/internal/funding/Catalog.js +19 -8
  276. package/dist/internal/funding/Catalog.js.map +1 -1
  277. package/dist/internal/funding/Deposit.d.ts +2 -2
  278. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  279. package/dist/internal/funding/Deposit.js +9 -8
  280. package/dist/internal/funding/Deposit.js.map +1 -1
  281. package/dist/internal/funding/DepositAddress.d.ts +4 -4
  282. package/dist/internal/funding/DepositAddress.d.ts.map +1 -1
  283. package/dist/internal/funding/DepositAddress.js +9 -8
  284. package/dist/internal/funding/DepositAddress.js.map +1 -1
  285. package/dist/internal/funding/Provider.d.ts +68 -13
  286. package/dist/internal/funding/Provider.d.ts.map +1 -1
  287. package/dist/internal/funding/Provider.js +42 -20
  288. package/dist/internal/funding/Provider.js.map +1 -1
  289. package/dist/internal/funding/Transfer.d.ts +13 -12
  290. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  291. package/dist/internal/funding/Transfer.js +23 -21
  292. package/dist/internal/funding/Transfer.js.map +1 -1
  293. package/dist/internal/funding/providers/stargate.d.ts +38 -0
  294. package/dist/internal/funding/providers/stargate.d.ts.map +1 -0
  295. package/dist/internal/funding/providers/stargate.js +277 -0
  296. package/dist/internal/funding/providers/stargate.js.map +1 -0
  297. package/dist/internal/index.d.ts +1 -0
  298. package/dist/internal/index.d.ts.map +1 -1
  299. package/dist/internal/index.js +1 -0
  300. package/dist/internal/index.js.map +1 -1
  301. package/dist/providers/index.d.ts +7 -0
  302. package/dist/providers/index.d.ts.map +1 -1
  303. package/dist/providers/index.js +3 -0
  304. package/dist/providers/index.js.map +1 -1
  305. package/package.json +3 -3
  306. package/src/App.test.ts +148 -0
  307. package/src/App.ts +45 -4
  308. package/src/Assets.test.ts +18 -0
  309. package/src/Assets.ts +22 -0
  310. package/src/Client.test-d.ts +311 -16
  311. package/src/Scope.test.ts +20 -0
  312. package/src/Scope.ts +16 -0
  313. package/src/admin/apps/api-keys.test.ts +75 -0
  314. package/src/admin/apps/api-keys.ts +64 -0
  315. package/src/admin/apps/earn-vaults.test.ts +143 -0
  316. package/src/admin/apps/earn-vaults.ts +84 -7
  317. package/src/admin/apps/organizations.ts +6 -2
  318. package/src/admin/ui/src/lib/api-keys.ts +19 -0
  319. package/src/admin/ui/src/routes/api-keys.tsx +125 -2
  320. package/src/admin/ui.generated.ts +71 -71
  321. package/src/apps/Zones.test.ts +4 -4
  322. package/src/apps/Zones.ts +4 -4
  323. package/src/apps/composition.test.ts +1 -1
  324. package/src/apps/data/App.test.ts +72 -0
  325. package/src/apps/data/App.ts +58 -10
  326. package/src/apps/data/ZoneSelection.test.ts +196 -0
  327. package/src/apps/data/ZoneSelection.ts +108 -0
  328. package/src/apps/data/routes/activities.test.ts +1843 -2
  329. package/src/apps/data/routes/activities.ts +2211 -1388
  330. package/src/apps/data/routes/balances.ts +134 -98
  331. package/src/apps/data/routes/blocks.test.ts +533 -0
  332. package/src/apps/data/routes/blocks.ts +36 -17
  333. package/src/apps/data/routes/earn.test.ts +187 -25
  334. package/src/apps/data/routes/earn.ts +228 -32
  335. package/src/apps/data/routes/earn.zone-access.test.ts +46 -0
  336. package/src/apps/data/routes/exchanges.test.ts +31 -1
  337. package/src/apps/data/routes/exchanges.ts +19 -15
  338. package/src/apps/data/routes/fee-amm.ts +18 -11
  339. package/src/apps/data/routes/receipts.ts +48 -17
  340. package/src/apps/data/routes/rpc.test.ts +75 -0
  341. package/src/apps/data/routes/rpc.ts +23 -9
  342. package/src/apps/data/routes/tokenlist.ts +21 -18
  343. package/src/apps/data/routes/tokens.test.ts +607 -0
  344. package/src/apps/data/routes/tokens.ts +312 -231
  345. package/src/apps/data/routes/transactions.ts +69 -30
  346. package/src/apps/data/routes/transfers.test.ts +424 -3
  347. package/src/apps/data/routes/transfers.ts +57 -36
  348. package/src/apps/data/routes/valuation.ts +94 -78
  349. package/src/apps/data/routes/webhooks.ts +6 -2
  350. package/src/apps/data/routes/zones.test.ts +21 -15
  351. package/src/apps/data/routes/zones.ts +1 -1
  352. package/src/apps/funding/routes/chains.test.ts +211 -7
  353. package/src/apps/funding/routes/chains.ts +19 -12
  354. package/src/apps/funding/routes/deposit-addresses.test.ts +416 -0
  355. package/src/apps/funding/routes/deposit-addresses.ts +101 -84
  356. package/src/apps/funding/routes/providers.ts +9 -5
  357. package/src/apps/funding/routes/quotes.test.ts +42 -0
  358. package/src/apps/funding/routes/quotes.ts +8 -5
  359. package/src/apps/funding/routes/transfers.test.ts +430 -40
  360. package/src/apps/funding/routes/transfers.ts +76 -59
  361. package/src/apps/management/Billing.test.ts +26 -0
  362. package/src/apps/management/Billing.ts +24 -7
  363. package/src/apps/management/routes/api-keys.test.ts +398 -0
  364. package/src/apps/management/routes/api-keys.ts +481 -145
  365. package/src/apps/management/routes/billing.test.ts +209 -0
  366. package/src/apps/management/routes/billing.ts +254 -177
  367. package/src/apps/management/routes/faucet.test.ts +39 -0
  368. package/src/apps/management/routes/faucet.ts +75 -41
  369. package/src/apps/management/routes/invitations.test.ts +216 -0
  370. package/src/apps/management/routes/invitations.ts +186 -109
  371. package/src/apps/management/routes/invite-links.test.ts +263 -1
  372. package/src/apps/management/routes/invite-links.ts +295 -214
  373. package/src/apps/management/routes/me.test.ts +49 -0
  374. package/src/apps/management/routes/me.ts +42 -32
  375. package/src/apps/management/routes/members.test.ts +124 -1
  376. package/src/apps/management/routes/members.ts +81 -51
  377. package/src/apps/management/routes/orgs.test.ts +176 -0
  378. package/src/apps/management/routes/orgs.ts +101 -59
  379. package/src/apps/management/routes/projects.test.ts +183 -0
  380. package/src/apps/management/routes/projects.ts +124 -60
  381. package/src/apps/management/routes/scopes.test.ts +44 -0
  382. package/src/apps/management/routes/scopes.ts +37 -28
  383. package/src/apps/management/routes/usage.test.ts +92 -0
  384. package/src/apps/management/routes/usage.ts +186 -151
  385. package/src/apps/management/routes/verified-token-requests.test.ts +114 -0
  386. package/src/apps/management/routes/verified-token-requests.ts +166 -133
  387. package/src/apps/management/routes/webhooks.test.ts +234 -0
  388. package/src/apps/management/routes/webhooks.ts +191 -78
  389. package/src/apps/mpp/internal/Adapter.ts +13 -1
  390. package/src/db/Db.test.ts +41 -0
  391. package/src/db/Db.ts +75 -1
  392. package/src/db/Schema.ts +33 -5
  393. package/src/db/tables/earnVaults.test.ts +22 -0
  394. package/src/db/tables/earnVaults.ts +11 -1
  395. package/src/db/tables/fundingCatalog.test.ts +16 -2
  396. package/src/db/tables/fundingCatalog.ts +9 -3
  397. package/src/db/tables/webhookDeliveries.ts +13 -0
  398. package/src/db/tables/webhookQueueCompletions.ts +55 -0
  399. package/src/db/tables/webhookQueueEvents.ts +95 -61
  400. package/src/db/tables/webhookSubscriptions.ts +45 -6
  401. package/src/handlers/internal/utils.test.ts +53 -0
  402. package/src/handlers/internal/utils.ts +15 -0
  403. package/src/internal/Auth.ts +35 -9
  404. package/src/internal/Cache.ts +14 -7
  405. package/src/internal/EarnVaults.capabilities.test.ts +33 -0
  406. package/src/internal/EarnVaults.test.ts +79 -0
  407. package/src/internal/EarnVaults.ts +277 -0
  408. package/src/internal/Fetch.test.ts +46 -0
  409. package/src/internal/Fetch.ts +56 -0
  410. package/src/internal/Log.test.ts +21 -0
  411. package/src/internal/Log.ts +20 -5
  412. package/src/internal/OpenApi.ts +177 -51
  413. package/src/internal/Response.test.ts +95 -0
  414. package/src/internal/Response.ts +41 -3
  415. package/src/internal/Schema.test.ts +18 -0
  416. package/src/internal/Schema.ts +19 -9
  417. package/src/internal/Store.test.ts +364 -0
  418. package/src/internal/Store.ts +153 -27
  419. package/src/internal/Tidx.test.ts +21 -1
  420. package/src/internal/Tidx.ts +23 -6
  421. package/src/internal/Timeout.test.ts +35 -0
  422. package/src/internal/Timeout.ts +73 -0
  423. package/src/internal/Timing.ts +16 -0
  424. package/src/internal/WebhookDestination.ts +3 -0
  425. package/src/internal/Webhooks.test.ts +122 -8
  426. package/src/internal/Webhooks.ts +59 -29
  427. package/src/internal/funding/Action.test.ts +41 -1
  428. package/src/internal/funding/Action.ts +69 -41
  429. package/src/internal/funding/Catalog.test.ts +34 -0
  430. package/src/internal/funding/Catalog.ts +28 -11
  431. package/src/internal/funding/Deposit.ts +71 -64
  432. package/src/internal/funding/DepositAddress.ts +50 -43
  433. package/src/internal/funding/Provider.test.ts +73 -0
  434. package/src/internal/funding/Provider.ts +246 -170
  435. package/src/internal/funding/Transfer.ts +133 -110
  436. package/src/internal/funding/providers/index.test.ts +22 -0
  437. package/src/internal/funding/providers/relay.test.ts +2 -2
  438. package/src/internal/funding/providers/stargate.test.ts +163 -0
  439. package/src/internal/funding/providers/stargate.ts +409 -0
  440. package/src/internal/index.test.ts +1 -0
  441. package/src/internal/index.ts +1 -0
  442. package/src/providers/index.test.ts +1 -0
  443. package/src/providers/index.ts +9 -0
@@ -3,7 +3,7 @@
3
3
  import { Address, Hex, Value as core_Value } from 'ox'
4
4
  import { FxOracle } from 'tapimo/apps'
5
5
  import { createClient, encodeAbiParameters, encodeEventTopics, http } from 'viem'
6
- import { sendTransactionSync } from 'viem/actions'
6
+ import { getLogs, sendTransactionSync } from 'viem/actions'
7
7
  import { Abis, Actions, Addresses } from 'viem/tempo'
8
8
 
9
9
  import * as TestApp from '../../../../test/App.js'
@@ -143,6 +143,9 @@ describe('GET /addresses/:address/activities', () => {
143
143
  expect(
144
144
  Activities.schema.getAddressActivities.Query.safeParse({ include: 'token' }).success,
145
145
  ).toBe(false)
146
+ expect(
147
+ Activities.schema.getAddressActivities.Query.parse({ include: 'zones' }).include,
148
+ ).toStrictEqual(['zones'])
146
149
  })
147
150
 
148
151
  test('returns a newest-first, schema-valid feed with timing', async () => {
@@ -536,6 +539,7 @@ describe('GET /transactions/:transactionHash/activities', () => {
536
539
  )
537
540
 
538
541
  const body = await TestApp.json(response, Activities.schema.getTransactionActivities.Response)
542
+ expect(body.chainId).toBe(runtime.chainId)
539
543
  expect(body.data.length).toBeGreaterThan(0)
540
544
  // Every classified item belongs to the requested transaction.
541
545
  expect(body.data.every((item) => item.transactionHash === transactionHash)).toBe(true)
@@ -607,6 +611,249 @@ describe('GET /transactions/:transactionHash/activities', () => {
607
611
  const response = await transactionActivitiesRequest(`0x${'0'.repeat(64)}`)
608
612
  expect(response.status).toBe(404)
609
613
  })
614
+
615
+ test.skipIf(runtime.mode !== 'localnet')(
616
+ 'infers indexed Zone activity by transaction and address',
617
+ async () => {
618
+ const account = Tempo.accounts[0]
619
+ const [zoneCreated] = await getLogs(Tempo.client, {
620
+ event: {
621
+ inputs: [
622
+ { indexed: true, name: 'zoneId', type: 'uint32' },
623
+ { indexed: true, name: 'portal', type: 'address' },
624
+ { indexed: false, name: 'initialToken', type: 'address' },
625
+ { indexed: false, name: 'admin', type: 'address' },
626
+ { indexed: false, name: 'sequencer', type: 'address' },
627
+ { indexed: false, name: 'verifier', type: 'address' },
628
+ { indexed: false, name: 'genesisBlockHash', type: 'bytes32' },
629
+ { indexed: false, name: 'genesisTempoBlockHash', type: 'bytes32' },
630
+ { indexed: false, name: 'genesisTempoBlockNumber', type: 'uint64' },
631
+ ],
632
+ name: 'ZoneCreated',
633
+ type: 'event',
634
+ },
635
+ fromBlock: 0n,
636
+ strict: true,
637
+ toBlock: 'latest',
638
+ })
639
+ expect(zoneCreated).toBeDefined()
640
+ if (!zoneCreated) return
641
+
642
+ const zone = TestApp.zone({
643
+ chainId: runtime.zone.chainId,
644
+ rpcUrl: runtime.zone.internalRpcUrl,
645
+ sourceChainId: runtime.chainId,
646
+ tidxUrl: runtime.tidxUrl,
647
+ })
648
+ const zoneClient = createClient({
649
+ account,
650
+ chain: zone,
651
+ pollingInterval: 100,
652
+ transport: http(runtime.zone.internalRpcUrl),
653
+ })
654
+ const balanceBefore = await Actions.token.getBalance(zoneClient, {
655
+ account: account.address,
656
+ token: Addresses.pathUsd,
657
+ })
658
+ const zoneDeposit = await Actions.zone.depositSync(Tempo.getClient({ account }), {
659
+ amount: 1_000_000n,
660
+ portalAddress: zoneCreated.args.portal,
661
+ token: Addresses.pathUsd,
662
+ zoneId: zoneCreated.args.zoneId,
663
+ })
664
+
665
+ let balance = balanceBefore
666
+ const balanceDeadline = Date.now() + 60_000
667
+ while (balance.amount <= balanceBefore.amount && Date.now() < balanceDeadline) {
668
+ await new Promise((resolve) => setTimeout(resolve, 100))
669
+ balance = await Actions.token.getBalance(zoneClient, {
670
+ account: account.address,
671
+ token: Addresses.pathUsd,
672
+ })
673
+ }
674
+ expect(balance.amount).toBeGreaterThan(balanceBefore.amount)
675
+
676
+ const { receipt } = await Actions.token.transferSync(zoneClient, {
677
+ amount: 1n,
678
+ to: Tempo.accounts[1].address,
679
+ token: Addresses.pathUsd,
680
+ })
681
+ const zoneReader = {
682
+ ...TestApp.key,
683
+ id: 'key_inferred_zone',
684
+ scopes: [...TestApp.key.scopes, `zone:${runtime.zone.chainId}:read`],
685
+ token: 'secret_inferred_zone',
686
+ } satisfies TestApp.kvStore.Key
687
+ const client = TestApp.client({
688
+ auth: { keys: [zoneReader] },
689
+ zones: [zone],
690
+ })
691
+ let response = await client.v1.transactions[':transactionHash'].activities.$get(
692
+ {
693
+ param: { transactionHash: receipt.transactionHash },
694
+ query: { chainId: String(runtime.chainId), include: 'zones' },
695
+ },
696
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
697
+ )
698
+ const deadline = Date.now() + 60_000
699
+ while ((response.status === 404 || response.status === 502) && Date.now() < deadline) {
700
+ await new Promise((resolve) => setTimeout(resolve, 500))
701
+ response = await client.v1.transactions[':transactionHash'].activities.$get(
702
+ {
703
+ param: { transactionHash: receipt.transactionHash },
704
+ query: { chainId: String(runtime.chainId), include: 'zones' },
705
+ },
706
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
707
+ )
708
+ }
709
+ const body = await TestApp.json(response, Activities.schema.getTransactionActivities.Response)
710
+
711
+ expect(response.status).toBe(200)
712
+ expect(response.headers.get('cache-control')).toBe('no-store')
713
+ expect(body.chainId).toBe(runtime.zone.chainId)
714
+ const cached = await client.v1.transactions[':transactionHash'].activities.$get(
715
+ {
716
+ param: { transactionHash: receipt.transactionHash },
717
+ query: { chainId: String(runtime.chainId), include: 'zones' },
718
+ },
719
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
720
+ )
721
+ expect(cached.status).toBe(200)
722
+ expect(cached.headers.get('server-timing')).not.toContain('tidx;dur=')
723
+
724
+ const zoneOnlyTransaction = await client.v1.transactions[':transactionHash'].activities.$get(
725
+ {
726
+ param: { transactionHash: receipt.transactionHash },
727
+ query: { chainId: String(runtime.zone.chainId) },
728
+ },
729
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
730
+ )
731
+ const zoneOnlyTransactionBody = await TestApp.json(
732
+ zoneOnlyTransaction,
733
+ Activities.schema.getTransactionActivities.Response,
734
+ )
735
+ expect(zoneOnlyTransaction.status).toBe(200)
736
+ expect(zoneOnlyTransactionBody.chainId).toBe(runtime.zone.chainId)
737
+
738
+ let parentTransaction = await client.v1.transactions[':transactionHash'].activities.$get(
739
+ {
740
+ param: { transactionHash: zoneDeposit.receipt.transactionHash },
741
+ query: { chainId: String(runtime.chainId), include: 'zones' },
742
+ },
743
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
744
+ )
745
+ const parentDeadline = Date.now() + 60_000
746
+ while (
747
+ (parentTransaction.status === 404 || parentTransaction.status === 502) &&
748
+ Date.now() < parentDeadline
749
+ ) {
750
+ await new Promise((resolve) => setTimeout(resolve, 500))
751
+ parentTransaction = await client.v1.transactions[':transactionHash'].activities.$get(
752
+ {
753
+ param: { transactionHash: zoneDeposit.receipt.transactionHash },
754
+ query: { chainId: String(runtime.chainId), include: 'zones' },
755
+ },
756
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
757
+ )
758
+ }
759
+ const parentTransactionBody = await TestApp.json(
760
+ parentTransaction,
761
+ Activities.schema.getTransactionActivities.Response,
762
+ )
763
+ expect(parentTransaction.status).toBe(200)
764
+ expect(parentTransactionBody.chainId).toBe(runtime.chainId)
765
+
766
+ const missingHash = `0x${'ff'.repeat(32)}` as Hex.Hex
767
+ const getMissing = () =>
768
+ client.v1.transactions[':transactionHash'].activities.$get(
769
+ {
770
+ param: { transactionHash: missingHash },
771
+ query: { chainId: String(runtime.chainId), include: 'zones' },
772
+ },
773
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
774
+ )
775
+ const missing = [await getMissing(), await getMissing()]
776
+ expect(missing.map(({ status }) => status)).toStrictEqual([404, 404])
777
+ expect(
778
+ missing.every((item) => item.headers.get('server-timing')?.includes('tidx;dur=')),
779
+ ).toBe(true)
780
+
781
+ const transfer = body.data.find((item) => item.type === 'transfer')
782
+ expect(transfer?.transactionHash).toBe(receipt.transactionHash)
783
+ if (transfer?.type !== 'transfer') return
784
+ expect(transfer.data.recipient).toBe(Tempo.accounts[1].address.toLowerCase())
785
+
786
+ const zoneAddressActivities = await (async () => {
787
+ const deadline = Date.now() + 60_000
788
+ while (Date.now() < deadline) {
789
+ const response = await client.v1.addresses[':address'].activities.$get(
790
+ {
791
+ param: { address: account.address },
792
+ query: {
793
+ chainId: String(runtime.chainId),
794
+ include: 'zones',
795
+ limit: '50',
796
+ },
797
+ },
798
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
799
+ )
800
+ if (response.status === 200) {
801
+ const body = await TestApp.json(
802
+ response,
803
+ Activities.schema.getAddressActivities.Response,
804
+ )
805
+ const hashes = body.data.flatMap((item) =>
806
+ item.type === 'group'
807
+ ? item.data.items.map(({ transactionHash }) => transactionHash)
808
+ : [item.transactionHash],
809
+ )
810
+ if (
811
+ hashes.includes(receipt.transactionHash) &&
812
+ hashes.includes(zoneDeposit.receipt.transactionHash)
813
+ )
814
+ return { body, response }
815
+ }
816
+ await new Promise((resolve) => setTimeout(resolve, 500))
817
+ }
818
+ throw new Error('Timed out waiting for inferred Zone address activity')
819
+ })()
820
+ const addressTransfer = zoneAddressActivities.body.data.find(
821
+ (item) => item.type !== 'group' && item.transactionHash === receipt.transactionHash,
822
+ )
823
+ expect(zoneAddressActivities.response.headers.get('cache-control')).toBe('no-store')
824
+ expect(addressTransfer?.chainId).toBe(runtime.zone.chainId)
825
+ const parentActivity = zoneAddressActivities.body.data.find(
826
+ (item) =>
827
+ item.type !== 'group' && item.transactionHash === zoneDeposit.receipt.transactionHash,
828
+ )
829
+ expect(parentActivity?.chainId).toBe(runtime.chainId)
830
+
831
+ const zoneOnlyActivities = await client.v1.addresses[':address'].activities.$get(
832
+ {
833
+ param: { address: account.address },
834
+ query: { chainId: String(runtime.zone.chainId), limit: '50' },
835
+ },
836
+ { headers: { authorization: `Bearer ${zoneReader.token}` } },
837
+ )
838
+ const zoneOnlyActivitiesBody = await TestApp.json(
839
+ zoneOnlyActivities,
840
+ Activities.schema.getAddressActivities.Response,
841
+ )
842
+ expect(zoneOnlyActivities.status).toBe(200)
843
+ expect(
844
+ zoneOnlyActivitiesBody.data.some(
845
+ (item) => item.type !== 'group' && item.transactionHash === receipt.transactionHash,
846
+ ),
847
+ ).toBe(true)
848
+ expect(
849
+ zoneOnlyActivitiesBody.data.some(
850
+ (item) =>
851
+ item.type !== 'group' && item.transactionHash === zoneDeposit.receipt.transactionHash,
852
+ ),
853
+ ).toBe(false)
854
+ },
855
+ 180_000,
856
+ )
610
857
  })
611
858
 
612
859
  describe('Activities.list classification', () => {
@@ -627,6 +874,192 @@ describe('Activities.list classification', () => {
627
874
  }
628
875
  }
629
876
 
877
+ function pagedProvider(rows: Activities.Row[]): Activities.Provider {
878
+ return {
879
+ cursorForGroup: (group) => `${group.blockNum}:${group.txIdx}`,
880
+ fetchRows: async ({ cursor, limit }) => {
881
+ const [blockNum, txIdx] = cursor?.split(':').map(Number) ?? []
882
+ return {
883
+ rows: rows
884
+ .filter(
885
+ (row) =>
886
+ blockNum === undefined ||
887
+ row.blockNum < blockNum ||
888
+ (row.blockNum === blockNum && row.txIdx < txIdx!),
889
+ )
890
+ .slice(0, limit + 1),
891
+ }
892
+ },
893
+ }
894
+ }
895
+
896
+ function unknownRow(options: {
897
+ blockNum: number
898
+ blockTimestamp: number
899
+ hash: string
900
+ }): Activities.Row {
901
+ return {
902
+ blockNum: options.blockNum,
903
+ blockTimestamp: options.blockTimestamp,
904
+ data: '0x',
905
+ feePayer: null,
906
+ logAddress: '0x2222222222222222222222222222222222222222',
907
+ logIdx: 0,
908
+ selector: null,
909
+ source: 'q1',
910
+ topic1: null,
911
+ topic2: null,
912
+ topic3: null,
913
+ txHash: `0x${options.hash.repeat(64)}` as Hex.Hex,
914
+ txIdx: 0,
915
+ txSender: viewer,
916
+ }
917
+ }
918
+
919
+ test('classifies every transfer row of a receipt-less (Q2) transaction', async () => {
920
+ const sender = '0x9999999999999999999999999999999999999999' as Address.Address
921
+ const token = '0x2222222222222222222222222222222222222222' as Address.Address
922
+ const q2Row = (options: { amount: bigint; logIdx: number }): Activities.Row => ({
923
+ blockNum: 100,
924
+ blockTimestamp: 1_700_000_000,
925
+ data: null,
926
+ feePayer: null,
927
+ logAddress: token,
928
+ logIdx: options.logIdx,
929
+ selector: null,
930
+ source: 'q2',
931
+ topic1: null,
932
+ topic2: null,
933
+ topic3: null,
934
+ transferAmount: options.amount,
935
+ transferFrom: sender,
936
+ transferTo: viewer,
937
+ transferToken: token,
938
+ txHash: `0x${'e'.repeat(64)}` as Hex.Hex,
939
+ txIdx: 0,
940
+ txSender: sender,
941
+ })
942
+ const { items } = await Activities.list(
943
+ provider([q2Row({ amount: 10n, logIdx: 0 }), q2Row({ amount: 20n, logIdx: 1 })]),
944
+ { address: viewer, store: store() },
945
+ )
946
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'transfer'])
947
+ expect(new Set(items.map((item) => item.id)).size).toBe(2)
948
+ })
949
+
950
+ test('merges chain pages and retains each provider cursor', async () => {
951
+ const providers = [
952
+ {
953
+ chainId: 10,
954
+ provider: pagedProvider([
955
+ unknownRow({ blockNum: 30, blockTimestamp: 300, hash: 'a' }),
956
+ unknownRow({ blockNum: 10, blockTimestamp: 100, hash: 'c' }),
957
+ ]),
958
+ },
959
+ {
960
+ chainId: 20,
961
+ provider: pagedProvider([unknownRow({ blockNum: 20, blockTimestamp: 200, hash: 'b' })]),
962
+ },
963
+ ]
964
+ const first = await Activities.listChains({
965
+ address: viewer,
966
+ limit: 2,
967
+ providers,
968
+ store: store(),
969
+ })
970
+ const second = await Activities.listChains({
971
+ address: viewer,
972
+ cursor: first.nextCursor!,
973
+ limit: 2,
974
+ providers,
975
+ store: store(),
976
+ })
977
+
978
+ expect(first.items.map((item) => [item.chainId, item.timestamp])).toStrictEqual([
979
+ [10, '1970-01-01T00:05:00.000Z'],
980
+ [20, '1970-01-01T00:03:20.000Z'],
981
+ ])
982
+ expect(first.nextCursor).not.toBeNull()
983
+ expect(second.items.map((item) => item.chainId)).toStrictEqual([10])
984
+ expect(second.nextCursor).toBeNull()
985
+ })
986
+
987
+ test('retains unused chain buffers while filling a grouped page', async () => {
988
+ const signer = '0x3333333333333333333333333333333333333333' as Address.Address
989
+ const calls = new Map<number, number>()
990
+ const providers = [
991
+ { chainId: 10, hashes: ['1', '3', '5'], timestamps: [600, 400, 200] },
992
+ { chainId: 20, hashes: ['2', '4', '6'], timestamps: [500, 300, 100] },
993
+ ].map(({ chainId, hashes, timestamps }) => {
994
+ const source = pagedProvider(
995
+ timestamps.map((blockTimestamp, index) => ({
996
+ ...unknownRow({ blockNum: blockTimestamp, blockTimestamp, hash: hashes[index]! }),
997
+ feePayer: viewer,
998
+ txSender: signer,
999
+ })),
1000
+ )
1001
+ return {
1002
+ chainId,
1003
+ provider: {
1004
+ cursorForGroup: (group) => source.cursorForGroup(group),
1005
+ fetchRows: async (options) => {
1006
+ calls.set(chainId, (calls.get(chainId) ?? 0) + 1)
1007
+ return source.fetchRows(options)
1008
+ },
1009
+ } satisfies Activities.Provider,
1010
+ }
1011
+ })
1012
+
1013
+ const result = await Activities.listChains({
1014
+ address: viewer,
1015
+ group: true,
1016
+ limit: 2,
1017
+ providers,
1018
+ store: store(),
1019
+ })
1020
+
1021
+ expect(result.items.map((item) => item.timestamp)).toStrictEqual([
1022
+ '1970-01-01T00:10:00.000Z',
1023
+ '1970-01-01T00:08:20.000Z',
1024
+ '1970-01-01T00:06:40.000Z',
1025
+ '1970-01-01T00:05:00.000Z',
1026
+ '1970-01-01T00:03:20.000Z',
1027
+ '1970-01-01T00:01:40.000Z',
1028
+ ])
1029
+ expect([...calls]).toStrictEqual([
1030
+ [10, 2],
1031
+ [20, 2],
1032
+ ])
1033
+ expect(result.nextCursor).toBeNull()
1034
+ })
1035
+
1036
+ test('does not fold access-key activity across chains', async () => {
1037
+ const signer = '0x3333333333333333333333333333333333333333' as Address.Address
1038
+ const providers = [10, 20].map((chainId) => ({
1039
+ chainId,
1040
+ provider: pagedProvider([
1041
+ {
1042
+ ...unknownRow({ blockNum: chainId, blockTimestamp: 300, hash: String(chainId / 10) }),
1043
+ feePayer: viewer,
1044
+ txSender: signer,
1045
+ },
1046
+ ]),
1047
+ }))
1048
+ const result = await Activities.listChains({
1049
+ address: viewer,
1050
+ providers,
1051
+ store: store(),
1052
+ })
1053
+ const grouped = Activities.groupByAccessKey(
1054
+ result.items.map((item) => Activities.schema.Item.parse(item)),
1055
+ )
1056
+
1057
+ expect(grouped.map((item) => [item.chainId, item.type])).toStrictEqual([
1058
+ [10, 'unknown'],
1059
+ [20, 'unknown'],
1060
+ ])
1061
+ })
1062
+
630
1063
  test('classifies an unrecognized Q1 tx as `unknown` with decoded events', async () => {
631
1064
  // A gas-fee Transfer to the fee manager decodes as a known `Transfer`
632
1065
  // event, but the classifier excludes fee-manager transfers, so the tx has
@@ -795,7 +1228,31 @@ describe('Activities.list precompile classification', () => {
795
1228
  logIdx: number
796
1229
  }): Activities.Row {
797
1230
  type Param = { indexed?: boolean; name?: string; type: string }
798
- const abi = [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault]
1231
+ // Standard ERC-4626 events, mirrored from the classifier's decode ABI.
1232
+ const erc4626Events = [
1233
+ {
1234
+ inputs: [
1235
+ { indexed: true, name: 'sender', type: 'address' },
1236
+ { indexed: true, name: 'owner', type: 'address' },
1237
+ { indexed: false, name: 'assets', type: 'uint256' },
1238
+ { indexed: false, name: 'shares', type: 'uint256' },
1239
+ ],
1240
+ name: 'Deposit',
1241
+ type: 'event',
1242
+ },
1243
+ {
1244
+ inputs: [
1245
+ { indexed: true, name: 'sender', type: 'address' },
1246
+ { indexed: true, name: 'receiver', type: 'address' },
1247
+ { indexed: true, name: 'owner', type: 'address' },
1248
+ { indexed: false, name: 'assets', type: 'uint256' },
1249
+ { indexed: false, name: 'shares', type: 'uint256' },
1250
+ ],
1251
+ name: 'Withdraw',
1252
+ type: 'event',
1253
+ },
1254
+ ] as const
1255
+ const abi = [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault, ...erc4626Events]
799
1256
  const event = abi.find((a) => a.type === 'event' && a.name === options.eventName) as {
800
1257
  inputs: readonly Param[]
801
1258
  }
@@ -1483,4 +1940,1388 @@ describe('Activities.list precompile classification', () => {
1483
1940
  if (items[0]!.type !== 'initialized') return
1484
1941
  expect(items[0]!.data.height).toBe('42')
1485
1942
  })
1943
+
1944
+ test('keeps duplicate identical payments as separate activities', async () => {
1945
+ const recipient = '0x7777777777777777777777777777777777777777' as Address.Address
1946
+ const items = await classify([
1947
+ transferRow({ from: viewer, logIdx: 0, to: recipient, token: tokenA, value: 10n }),
1948
+ transferRow({ from: viewer, logIdx: 1, to: recipient, token: tokenA, value: 10n }),
1949
+ ])
1950
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'transfer'])
1951
+ expect(new Set(items.map((item) => item.id)).size).toBe(2)
1952
+ })
1953
+
1954
+ test('folds zero-address transfer duplicates into explicit mint and burn events', async () => {
1955
+ const zero = '0x0000000000000000000000000000000000000000' as Address.Address
1956
+ const minted = await classify([
1957
+ transferRow({ from: zero, logIdx: 0, to: viewer, token: tokenA, value: 10n }),
1958
+ logRow({ address: tokenA, args: { amount: 10n, to: viewer }, eventName: 'Mint', logIdx: 1 }),
1959
+ ])
1960
+ const burned = await classify([
1961
+ transferRow({ from: viewer, logIdx: 0, to: zero, token: tokenA, value: 10n }),
1962
+ logRow({
1963
+ address: tokenA,
1964
+ args: { amount: 10n, from: viewer },
1965
+ eventName: 'Burn',
1966
+ logIdx: 1,
1967
+ }),
1968
+ ])
1969
+ expect(minted.map((item) => item.type)).toEqual(['mint'])
1970
+ expect(burned.map((item) => item.type)).toEqual(['burn'])
1971
+ })
1972
+
1973
+ test('classifies a sender-initiated mint to another recipient', async () => {
1974
+ const recipient = '0x7777777777777777777777777777777777777777' as Address.Address
1975
+ const items = await classify([
1976
+ logRow({
1977
+ address: tokenA,
1978
+ args: { amount: 10n, to: recipient },
1979
+ eventName: 'Mint',
1980
+ logIdx: 0,
1981
+ }),
1982
+ ])
1983
+ expect(items).toHaveLength(1)
1984
+ expect(items[0]?.type === 'mint' && items[0].data.recipient).toBe(recipient)
1985
+ })
1986
+
1987
+ test('keeps a payment batched after a share-burn withdrawal', async () => {
1988
+ const asset = '0x5555555555555555555555555555555555555555' as Address.Address
1989
+ const recipient = '0x7777777777777777777777777777777777777777' as Address.Address
1990
+ const zero = '0x0000000000000000000000000000000000000000' as Address.Address
1991
+ const items = await classify([
1992
+ transferRow({ from: viewer, logIdx: 0, to: zero, token: tokenA, value: 100n }),
1993
+ transferRow({ from: tokenA, logIdx: 1, to: viewer, token: asset, value: 100n }),
1994
+ transferRow({ from: viewer, logIdx: 2, to: recipient, token: asset, value: 100n }),
1995
+ ])
1996
+ expect(items.map((item) => item.type)).toEqual(['burn', 'transfer', 'transfer'])
1997
+ const payment = items[2]
1998
+ expect(payment?.type).toBe('transfer')
1999
+ if (payment?.type !== 'transfer') return
2000
+ expect(payment.data.direction).toBe('out')
2001
+ expect(payment.data.recipient).toBe(recipient)
2002
+ })
2003
+
2004
+ test('folds vault token legs without hiding an independent payment', async () => {
2005
+ const asset = '0x4444444444444444444444444444444444444444' as Address.Address
2006
+ const recipient = '0x5555555555555555555555555555555555555555' as Address.Address
2007
+ const zero = '0x0000000000000000000000000000000000000000' as Address.Address
2008
+ const items = await classify([
2009
+ transferRow({ from: viewer, logIdx: 0, to: tokenA, token: asset, value: 100n }),
2010
+ transferRow({ from: zero, logIdx: 1, to: viewer, token: tokenA, value: 90n }),
2011
+ logRow({
2012
+ address: tokenA,
2013
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2014
+ eventName: 'Deposited',
2015
+ logIdx: 2,
2016
+ }),
2017
+ transferRow({ from: viewer, logIdx: 3, to: recipient, token: asset, value: 10n }),
2018
+ ])
2019
+ expect(items.map((item) => item.type)).toEqual(['assets-deposited', 'transfer'])
2020
+ expect(items[1]?.type === 'transfer' && items[1].data.recipient).toBe(recipient)
2021
+ })
2022
+
2023
+ test('keeps an independent vault deposit beside a private Earn deposit', async () => {
2024
+ const actionId = `0x${'c'.repeat(64)}` as Hex.Hex
2025
+ const vaultB = '0x4444444444444444444444444444444444444444' as Address.Address
2026
+ const items = await classify([
2027
+ logRow({
2028
+ address: tokenA,
2029
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2030
+ eventName: 'Deposited',
2031
+ logIdx: 0,
2032
+ }),
2033
+ logRow({
2034
+ address: tokenA,
2035
+ args: {
2036
+ actionId,
2037
+ earnShares: 90n,
2038
+ earnVault: tokenA,
2039
+ inputAmount: 100n,
2040
+ inputToken: tokenA,
2041
+ vaultAssets: 100n,
2042
+ zoneDepositHash: actionId,
2043
+ },
2044
+ eventName: 'EarnDeposit',
2045
+ logIdx: 1,
2046
+ }),
2047
+ logRow({
2048
+ address: vaultB,
2049
+ args: { assets: 50n, caller: viewer, earnShares: 45n, receiver: viewer },
2050
+ eventName: 'Deposited',
2051
+ logIdx: 2,
2052
+ }),
2053
+ ])
2054
+ expect(items.map((item) => item.type)).toEqual(['private-assets-deposited', 'assets-deposited'])
2055
+ expect(items[1]?.type === 'assets-deposited' && items[1].data.vault).toBe(vaultB)
2056
+ })
2057
+
2058
+ test('keeps two identical swaps through the same route distinct', async () => {
2059
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2060
+ const items = await classify([
2061
+ transferRow({ from: viewer, logIdx: 0, to: stablecoinDex, token: tokenA, value: 1_000n }),
2062
+ transferRow({ from: stablecoinDex, logIdx: 1, to: viewer, token: tokenB, value: 990n }),
2063
+ transferRow({ from: viewer, logIdx: 2, to: stablecoinDex, token: tokenA, value: 1_000n }),
2064
+ transferRow({ from: stablecoinDex, logIdx: 3, to: viewer, token: tokenB, value: 990n }),
2065
+ ])
2066
+ expect(items.map((item) => item.type)).toEqual(['swap', 'swap'])
2067
+ expect(new Set(items.map((item) => item.id)).size).toBe(2)
2068
+ })
2069
+
2070
+ test('keeps an independent approval beside a swap', async () => {
2071
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2072
+ const tokenC = '0x6666666666666666666666666666666666666666' as Address.Address
2073
+ const spender = '0x7777777777777777777777777777777777777777' as Address.Address
2074
+ const items = await classify([
2075
+ logRow({
2076
+ address: tokenA,
2077
+ args: { amount: 1_000n, owner: viewer, spender },
2078
+ eventName: 'Approval',
2079
+ logIdx: 0,
2080
+ }),
2081
+ transferRow({ from: viewer, logIdx: 1, to: stablecoinDex, token: tokenA, value: 1_000n }),
2082
+ transferRow({ from: stablecoinDex, logIdx: 2, to: viewer, token: tokenB, value: 990n }),
2083
+ logRow({
2084
+ address: tokenC,
2085
+ args: { amount: 7n, owner: viewer, spender },
2086
+ eventName: 'Approval',
2087
+ logIdx: 3,
2088
+ }),
2089
+ ])
2090
+ expect(items.map((item) => item.type)).toEqual(['swap', 'approval'])
2091
+ expect(items[1]?.type === 'approval' && items[1].data.sourceToken.address).toBe(tokenC)
2092
+ })
2093
+
2094
+ test('classifies a swap routed through a per-route account', async () => {
2095
+ const router = '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f' as Address.Address
2096
+ const executor = '0xccc88a9d1b4ed6b0eaba998850414b24f1c315be' as Address.Address
2097
+ const pool = '0x8888888888888888888888888888888888888888' as Address.Address
2098
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2099
+ // Route evidence: an undecoded event the router emits mid-swap.
2100
+ const routerLog: Activities.Row = {
2101
+ ...transferRow({ from: viewer, logIdx: 2, to: router, token: tokenA, value: 0n }),
2102
+ data: '0x',
2103
+ logAddress: router,
2104
+ selector: `0x${'9'.repeat(64)}`,
2105
+ topic1: null,
2106
+ topic2: null,
2107
+ }
2108
+ const items = await classify([
2109
+ logRow({
2110
+ address: tokenA,
2111
+ args: { amount: 1_105n, owner: viewer, spender: executor },
2112
+ eventName: 'Approval',
2113
+ logIdx: 0,
2114
+ }),
2115
+ transferRow({ from: viewer, logIdx: 1, to: router, token: tokenA, value: 1_105n }),
2116
+ routerLog,
2117
+ transferRow({ from: router, logIdx: 3, to: pool, token: tokenA, value: 1_105n }),
2118
+ transferRow({ from: pool, logIdx: 4, to: router, token: tokenB, value: 1_104n }),
2119
+ transferRow({ from: router, logIdx: 5, to: viewer, token: tokenB, value: 1_104n }),
2120
+ ])
2121
+ expect(items.map((item) => item.type)).toEqual(['swap'])
2122
+ expect(items[0]?.type).toBe('swap')
2123
+ if (items[0]?.type !== 'swap') return
2124
+ expect(items[0].data.sourceToken).toEqual({ address: tokenA, amount: '1105' })
2125
+ expect(items[0].data.destinationToken).toEqual({ address: tokenB, amount: '1104' })
2126
+ })
2127
+
2128
+ test('keeps payments through a passive counterparty as transfers', async () => {
2129
+ const merchant = '0x7777777777777777777777777777777777777777' as Address.Address
2130
+ const other = '0x9999999999999999999999999999999999999999' as Address.Address
2131
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2132
+ const items = await classify([
2133
+ transferRow({ from: viewer, logIdx: 0, to: merchant, token: tokenA, value: 100n }),
2134
+ transferRow({ from: merchant, logIdx: 1, to: other, token: tokenA, value: 30n }),
2135
+ transferRow({ from: merchant, logIdx: 2, to: viewer, token: tokenB, value: 50n }),
2136
+ ])
2137
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'transfer'])
2138
+ if (items[0]?.type !== 'transfer' || items[1]?.type !== 'transfer') return
2139
+ expect(items[0].data.direction).toBe('out')
2140
+ expect(items[1].data.direction).toBe('in')
2141
+ })
2142
+
2143
+ test('keeps interleaved DEX swaps by different swappers distinct', async () => {
2144
+ const other = '0x9999999999999999999999999999999999999999' as Address.Address
2145
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2146
+ const items = await classify([
2147
+ transferRow({ from: viewer, logIdx: 0, to: stablecoinDex, token: tokenA, value: 1_000n }),
2148
+ transferRow({ from: other, logIdx: 1, to: stablecoinDex, token: tokenA, value: 500n }),
2149
+ transferRow({ from: stablecoinDex, logIdx: 2, to: viewer, token: tokenB, value: 990n }),
2150
+ transferRow({ from: stablecoinDex, logIdx: 3, to: other, token: tokenB, value: 495n }),
2151
+ ])
2152
+ expect(items.map((item) => item.type)).toEqual(['swap', 'swap'])
2153
+ })
2154
+
2155
+ test('keeps a share-token payment beside a vault deposit of equal amounts', async () => {
2156
+ const asset = '0x4444444444444444444444444444444444444444' as Address.Address
2157
+ const recipient = '0x7777777777777777777777777777777777777777' as Address.Address
2158
+ const zero = '0x0000000000000000000000000000000000000000' as Address.Address
2159
+ const items = await classify([
2160
+ transferRow({ from: viewer, logIdx: 0, to: recipient, token: tokenA, value: 90n }),
2161
+ transferRow({ from: viewer, logIdx: 1, to: tokenA, token: asset, value: 100n }),
2162
+ transferRow({ from: zero, logIdx: 2, to: viewer, token: tokenA, value: 90n }),
2163
+ logRow({
2164
+ address: tokenA,
2165
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2166
+ eventName: 'Deposited',
2167
+ logIdx: 3,
2168
+ }),
2169
+ ])
2170
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'assets-deposited'])
2171
+ expect(items[0]?.type === 'transfer' && items[0].data.recipient).toBe(recipient)
2172
+ })
2173
+
2174
+ test('keeps an inbound payment beside a refundless session close', async () => {
2175
+ const sender = '0x9999999999999999999999999999999999999999' as Address.Address
2176
+ const payment = transferRow({ from: sender, logIdx: 0, to: viewer, token: tokenA, value: 100n })
2177
+ const items = await classify([
2178
+ payment,
2179
+ {
2180
+ ...payment,
2181
+ data: '0x',
2182
+ logAddress: channelReserve,
2183
+ logIdx: 1,
2184
+ selector: '0xf5a36fc00a96cbb9cf1f8f59299165e1d8ffffe94396d82904b4da524d16bbce',
2185
+ topic1: null,
2186
+ topic2: null,
2187
+ },
2188
+ ])
2189
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'session-closed'])
2190
+ expect(items[1]?.type === 'session-closed' && items[1].data.refund).toBeUndefined()
2191
+ })
2192
+
2193
+ test('folds the router funding leg into a private Earn deposit', async () => {
2194
+ const actionId = `0x${'c'.repeat(64)}` as Hex.Hex
2195
+ const router = '0x9999999999999999999999999999999999999999' as Address.Address
2196
+ const asset = '0x4444444444444444444444444444444444444444' as Address.Address
2197
+ const items = await classify([
2198
+ transferRow({ from: viewer, logIdx: 0, to: router, token: asset, value: 100n }),
2199
+ logRow({
2200
+ address: router,
2201
+ args: {
2202
+ actionId,
2203
+ earnShares: 90n,
2204
+ earnVault: tokenA,
2205
+ inputAmount: 100n,
2206
+ inputToken: asset,
2207
+ vaultAssets: 100n,
2208
+ zoneDepositHash: actionId,
2209
+ },
2210
+ eventName: 'EarnDeposit',
2211
+ logIdx: 1,
2212
+ }),
2213
+ ])
2214
+ expect(items.map((item) => item.type)).toEqual(['private-assets-deposited'])
2215
+ })
2216
+
2217
+ test('keeps a mint whose paired event decodes without a recipient', async () => {
2218
+ const recipient = '0x7777777777777777777777777777777777777777' as Address.Address
2219
+ const zero = '0x0000000000000000000000000000000000000000' as Address.Address
2220
+ const malformedMint = logRow({
2221
+ address: tokenA,
2222
+ args: { amount: 10n, to: viewer },
2223
+ eventName: 'Mint',
2224
+ logIdx: 2,
2225
+ })
2226
+ const items = await classify([
2227
+ transferRow({ from: viewer, logIdx: 0, to: recipient, token: tokenA, value: 5n }),
2228
+ transferRow({ from: zero, logIdx: 1, to: viewer, token: tokenA, value: 10n }),
2229
+ // The indexed recipient topic is stripped, so the Mint decodes without
2230
+ // `to` and must not fold away the zero-address transfer.
2231
+ { ...malformedMint, topic1: null },
2232
+ ])
2233
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'mint'])
2234
+ })
2235
+
2236
+ test('emits an external vault withdrawal paid out to the viewer', async () => {
2237
+ const other = '0x9999999999999999999999999999999999999999' as Address.Address
2238
+ const asset = '0x4444444444444444444444444444444444444444' as Address.Address
2239
+ const items = await classify([
2240
+ transferRow({ from: tokenA, logIdx: 0, to: viewer, token: asset, value: 100n }),
2241
+ logRow({
2242
+ address: tokenA,
2243
+ args: { assets: 100n, owner: other, receiver: other, sender: other, shares: 90n },
2244
+ eventName: 'Withdraw',
2245
+ logIdx: 1,
2246
+ }),
2247
+ ])
2248
+ expect(items.map((item) => item.type)).toEqual(['assets-withdrawn'])
2249
+ })
2250
+
2251
+ test('classifies a route delivery to a third party as a cross-token transfer', async () => {
2252
+ const router = '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f' as Address.Address
2253
+ const recipient = '0x7bc2d22a73706758505e222ed9d13a1a7f251490' as Address.Address
2254
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2255
+ const routerLog: Activities.Row = {
2256
+ ...transferRow({ from: viewer, logIdx: 1, to: router, token: tokenA, value: 0n }),
2257
+ data: '0x',
2258
+ logAddress: router,
2259
+ selector: `0x${'9'.repeat(64)}`,
2260
+ topic1: null,
2261
+ topic2: null,
2262
+ }
2263
+ const items = await classify([
2264
+ transferRow({ from: viewer, logIdx: 0, to: router, token: tokenA, value: 38_129n }),
2265
+ routerLog,
2266
+ transferRow({ from: router, logIdx: 2, to: recipient, token: tokenB, value: 38_127n }),
2267
+ ])
2268
+ expect(items.map((item) => item.type)).toEqual(['transfer'])
2269
+ const payment = items[0]
2270
+ if (payment?.type !== 'transfer') return
2271
+ expect(payment.data.recipient).toBe(recipient)
2272
+ expect(payment.data.sourceToken).toEqual({ address: tokenA, amount: '38129' })
2273
+ expect(payment.data.destinationToken).toEqual({ address: tokenB, amount: '38127' })
2274
+ })
2275
+
2276
+ test('folds the exact allowance granted to a vault into its deposit', async () => {
2277
+ const asset = '0x4444444444444444444444444444444444444444' as Address.Address
2278
+ const zero = '0x0000000000000000000000000000000000000000' as Address.Address
2279
+ const items = await classify([
2280
+ logRow({
2281
+ address: asset,
2282
+ args: { amount: 100n, owner: viewer, spender: tokenA },
2283
+ eventName: 'Approval',
2284
+ logIdx: 0,
2285
+ }),
2286
+ transferRow({ from: viewer, logIdx: 1, to: tokenA, token: asset, value: 100n }),
2287
+ transferRow({ from: zero, logIdx: 2, to: viewer, token: tokenA, value: 90n }),
2288
+ logRow({
2289
+ address: tokenA,
2290
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2291
+ eventName: 'Deposited',
2292
+ logIdx: 3,
2293
+ }),
2294
+ ])
2295
+ expect(items.map((item) => item.type)).toEqual(['assets-deposited'])
2296
+ })
2297
+
2298
+ test('keeps a payment and a later refund in another token as transfers', async () => {
2299
+ const merchant = '0x7777777777777777777777777777777777777777' as Address.Address
2300
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2301
+ const items = await classify([
2302
+ transferRow({ from: viewer, logIdx: 0, to: merchant, token: tokenA, value: 100n }),
2303
+ transferRow({ from: merchant, logIdx: 1, to: viewer, token: tokenB, value: 90n }),
2304
+ ])
2305
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'transfer'])
2306
+ })
2307
+
2308
+ test('folds an auto-flip order placement into its swap', async () => {
2309
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2310
+ const items = await classify([
2311
+ transferRow({ from: viewer, logIdx: 0, to: stablecoinDex, token: tokenA, value: 1_000n }),
2312
+ transferRow({ from: stablecoinDex, logIdx: 1, to: viewer, token: tokenB, value: 990n }),
2313
+ logRow({
2314
+ address: stablecoinDex,
2315
+ args: {
2316
+ amount: 25n,
2317
+ flipTick: 6,
2318
+ isBid: false,
2319
+ isFlipOrder: true,
2320
+ maker: viewer,
2321
+ orderId: 43n,
2322
+ tick: 5,
2323
+ token: tokenA,
2324
+ },
2325
+ eventName: 'OrderPlaced',
2326
+ logIdx: 2,
2327
+ }),
2328
+ ])
2329
+ expect(items.map((item) => item.type)).toEqual(['swap'])
2330
+ })
2331
+
2332
+ test('folds both fee legs into a RebalanceSwap', async () => {
2333
+ const tokenB = '0x4444444444444444444444444444444444444444' as Address.Address
2334
+ const items = await classify([
2335
+ transferRow({ from: viewer, logIdx: 0, to: feeManager, token: tokenA, value: 10n }),
2336
+ logRow({
2337
+ address: feeManager,
2338
+ args: {
2339
+ amountIn: 10n,
2340
+ amountOut: 9n,
2341
+ swapper: viewer,
2342
+ userToken: tokenB,
2343
+ validatorToken: tokenA,
2344
+ },
2345
+ eventName: 'RebalanceSwap',
2346
+ logIdx: 1,
2347
+ }),
2348
+ transferRow({ from: feeManager, logIdx: 2, to: viewer, token: tokenB, value: 9n }),
2349
+ ])
2350
+ expect(items.map((item) => item.type)).toEqual(['fee-rebalance-swap'])
2351
+ })
2352
+
2353
+ test('folds a CloseRequested refund into session-closed', async () => {
2354
+ const refund = transferRow({
2355
+ from: channelReserve,
2356
+ logIdx: 0,
2357
+ to: viewer,
2358
+ token: tokenA,
2359
+ value: 25n,
2360
+ })
2361
+ const items = await classify([
2362
+ refund,
2363
+ {
2364
+ ...refund,
2365
+ data: '0x',
2366
+ logAddress: channelReserve,
2367
+ logIdx: 1,
2368
+ selector: '0xf5a36fc00a96cbb9cf1f8f59299165e1d8ffffe94396d82904b4da524d16bbce',
2369
+ topic1: null,
2370
+ topic2: null,
2371
+ },
2372
+ ])
2373
+ expect(items.map((item) => item.type)).toEqual(['session-closed'])
2374
+ expect(items[0]?.type === 'session-closed' && items[0].data.refund?.amount).toBe('25')
2375
+ })
2376
+
2377
+ test('keeps batched channel settlements paired with their own payout legs', async () => {
2378
+ const payee = '0x7777777777777777777777777777777777777777' as Address.Address
2379
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2380
+ const channelA = `0x${'a'.repeat(64)}` as Hex.Hex
2381
+ const channelB = `0x${'b'.repeat(64)}` as Hex.Hex
2382
+ const items = await classify([
2383
+ transferRow({ from: channelReserve, logIdx: 0, to: payee, token: tokenA, value: 250n }),
2384
+ logRow({
2385
+ address: channelReserve,
2386
+ args: {
2387
+ channelId: channelA,
2388
+ cumulativeAmount: 250n,
2389
+ deltaPaid: 250n,
2390
+ newSettled: 250n,
2391
+ payee,
2392
+ payer: viewer,
2393
+ },
2394
+ eventName: 'Settled',
2395
+ logIdx: 1,
2396
+ }),
2397
+ transferRow({ from: channelReserve, logIdx: 2, to: payee, token: tokenB, value: 300n }),
2398
+ logRow({
2399
+ address: channelReserve,
2400
+ args: {
2401
+ channelId: channelB,
2402
+ cumulativeAmount: 300n,
2403
+ deltaPaid: 300n,
2404
+ newSettled: 300n,
2405
+ payee,
2406
+ payer: viewer,
2407
+ },
2408
+ eventName: 'Settled',
2409
+ logIdx: 3,
2410
+ }),
2411
+ ])
2412
+ expect(items.map((item) => item.type)).toEqual(['channel-settled', 'channel-settled'])
2413
+ if (items[0]?.type !== 'channel-settled' || items[1]?.type !== 'channel-settled') return
2414
+ expect(items[0].data.sourceToken.address).toBe(tokenA)
2415
+ expect(items[1].data.sourceToken.address).toBe(tokenB)
2416
+ })
2417
+
2418
+ test('keeps a role grant made after token creation', async () => {
2419
+ const otherAccount = '0x7777777777777777777777777777777777777777' as Address.Address
2420
+ const items = await classify([
2421
+ logRow({
2422
+ address: tokenA,
2423
+ args: { account: viewer, hasRole: true, role: `0x${'1'.repeat(64)}`, sender: viewer },
2424
+ eventName: 'RoleMembershipUpdated',
2425
+ logIdx: 0,
2426
+ }),
2427
+ logRow({
2428
+ address: tip20Factory,
2429
+ args: {
2430
+ admin: viewer,
2431
+ currency: 'USD',
2432
+ name: 'Test',
2433
+ quoteToken: tokenA,
2434
+ salt: `0x${'0'.repeat(64)}`,
2435
+ symbol: 'TST',
2436
+ token: tokenA,
2437
+ },
2438
+ eventName: 'TokenCreated',
2439
+ logIdx: 1,
2440
+ }),
2441
+ logRow({
2442
+ address: tokenA,
2443
+ args: {
2444
+ account: otherAccount,
2445
+ hasRole: true,
2446
+ role: `0x${'2'.repeat(64)}`,
2447
+ sender: viewer,
2448
+ },
2449
+ eventName: 'RoleMembershipUpdated',
2450
+ logIdx: 2,
2451
+ }),
2452
+ ])
2453
+ expect(items.map((item) => item.type)).toEqual(['token-created', 'role-membership-set'])
2454
+ expect(items[1]?.type === 'role-membership-set' && items[1].data.account).toBe(otherAccount)
2455
+ })
2456
+
2457
+ test('keeps a whitelist update made after its policy was created', async () => {
2458
+ const account = '0x7777777777777777777777777777777777777777' as Address.Address
2459
+ const items = await classify([
2460
+ logRow({
2461
+ address: tokenA,
2462
+ args: { account, allowed: true, policyId: 9n, updater: viewer },
2463
+ eventName: 'WhitelistUpdated',
2464
+ logIdx: 0,
2465
+ }),
2466
+ logRow({
2467
+ address: tokenA,
2468
+ args: { policyId: 9n, policyType: 1, updater: viewer },
2469
+ eventName: 'PolicyCreated',
2470
+ logIdx: 1,
2471
+ }),
2472
+ logRow({
2473
+ address: tokenA,
2474
+ args: { account, allowed: false, policyId: 9n, updater: viewer },
2475
+ eventName: 'WhitelistUpdated',
2476
+ logIdx: 2,
2477
+ }),
2478
+ ])
2479
+ expect(items.map((item) => item.type)).toEqual(['policy-created', 'whitelist-updated'])
2480
+ })
2481
+
2482
+ test('keeps an unrelated validator addition beside a rotation', async () => {
2483
+ const validator = '0x9999999999999999999999999999999999999999' as Address.Address
2484
+ const feeRecipient = '0xabababababababababababababababababababab' as Address.Address
2485
+ const items = await classify([
2486
+ logRow({
2487
+ address: accountKeychain,
2488
+ args: { index: 1n, validatorAddress: viewer },
2489
+ eventName: 'ValidatorDeactivated',
2490
+ logIdx: 0,
2491
+ }),
2492
+ logRow({
2493
+ address: accountKeychain,
2494
+ args: {
2495
+ egress: 'https://v.example.com:2',
2496
+ feeRecipient,
2497
+ index: 2n,
2498
+ ingress: 'https://v.example.com:1',
2499
+ publicKey: `0x${'2'.repeat(64)}`,
2500
+ validatorAddress: validator,
2501
+ },
2502
+ eventName: 'ValidatorAdded',
2503
+ logIdx: 1,
2504
+ }),
2505
+ logRow({
2506
+ address: accountKeychain,
2507
+ args: {
2508
+ caller: viewer,
2509
+ deactivatedIndex: 1n,
2510
+ egress: 'https://v.example.com:2',
2511
+ index: 2n,
2512
+ ingress: 'https://v.example.com:1',
2513
+ newPublicKey: `0x${'2'.repeat(64)}`,
2514
+ oldPublicKey: `0x${'1'.repeat(64)}`,
2515
+ validatorAddress: validator,
2516
+ },
2517
+ eventName: 'ValidatorRotated',
2518
+ logIdx: 2,
2519
+ }),
2520
+ logRow({
2521
+ address: accountKeychain,
2522
+ args: {
2523
+ egress: 'https://v.example.com:4',
2524
+ feeRecipient,
2525
+ index: 3n,
2526
+ ingress: 'https://v.example.com:3',
2527
+ publicKey: `0x${'4'.repeat(64)}`,
2528
+ validatorAddress: feeRecipient,
2529
+ },
2530
+ eventName: 'ValidatorAdded',
2531
+ logIdx: 3,
2532
+ }),
2533
+ ])
2534
+ expect(items.map((item) => item.type)).toEqual(['validator-rotated', 'validator-added'])
2535
+ })
2536
+
2537
+ test('keeps an access-key authorization beside a vault deposit', async () => {
2538
+ const publicKey = '0x9999999999999999999999999999999999999999' as Address.Address
2539
+ const items = await classify([
2540
+ logRow({
2541
+ address: tokenA,
2542
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2543
+ eventName: 'Deposited',
2544
+ logIdx: 0,
2545
+ }),
2546
+ logRow({
2547
+ address: accountKeychain,
2548
+ args: { account: viewer, expiry: 1_800_000_000n, publicKey, signatureType: 1 },
2549
+ eventName: 'KeyAuthorized',
2550
+ logIdx: 1,
2551
+ }),
2552
+ ])
2553
+ expect(items.map((item) => item.type)).toEqual(['assets-deposited', 'access-key-created'])
2554
+ })
2555
+
2556
+ test('keeps both directions of a receipt-less transaction', async () => {
2557
+ const other = '0x9999999999999999999999999999999999999999' as Address.Address
2558
+ const q2 = transferRow({ from: other, logIdx: 0, to: viewer, token: tokenA, value: 10n })
2559
+ const q3 = transferRow({ from: viewer, logIdx: 1, to: other, token: tokenA, value: 5n })
2560
+ const merged = Activities.mergeAndSort([], [q2], [q3])
2561
+ expect(merged.map((row) => row.logIdx)).toEqual([0, 1])
2562
+ })
2563
+
2564
+ test('keeps duplicate payments when one carries a memo', async () => {
2565
+ const recipient = '0x7777777777777777777777777777777777777777' as Address.Address
2566
+ const items = await classify([
2567
+ transferRow({ from: viewer, logIdx: 0, to: recipient, token: tokenA, value: 10n }),
2568
+ logRow({
2569
+ address: tokenA,
2570
+ args: { amount: 10n, from: viewer, memo: `0x${'1'.repeat(64)}`, to: recipient },
2571
+ eventName: 'TransferWithMemo',
2572
+ logIdx: 1,
2573
+ }),
2574
+ transferRow({ from: viewer, logIdx: 2, to: recipient, token: tokenA, value: 10n }),
2575
+ ])
2576
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'transfer'])
2577
+ })
2578
+
2579
+ test('keeps a settlement payout beside a refundless session close', async () => {
2580
+ const payee = viewer
2581
+ const closeRow = {
2582
+ ...transferRow({ from: channelReserve, logIdx: 0, to: viewer, token: tokenA, value: 0n }),
2583
+ data: '0x',
2584
+ logAddress: channelReserve,
2585
+ logIdx: 0,
2586
+ selector: '0xf5a36fc00a96cbb9cf1f8f59299165e1d8ffffe94396d82904b4da524d16bbce',
2587
+ topic1: null,
2588
+ topic2: null,
2589
+ }
2590
+ const items = await classify([
2591
+ closeRow,
2592
+ transferRow({ from: channelReserve, logIdx: 1, to: payee, token: tokenA, value: 250n }),
2593
+ logRow({
2594
+ address: channelReserve,
2595
+ args: {
2596
+ channelId: `0x${'c'.repeat(64)}`,
2597
+ cumulativeAmount: 250n,
2598
+ deltaPaid: 250n,
2599
+ newSettled: 250n,
2600
+ payee,
2601
+ payer: viewer,
2602
+ },
2603
+ eventName: 'Settled',
2604
+ logIdx: 2,
2605
+ }),
2606
+ ])
2607
+ expect(items.map((item) => item.type)).toEqual(['session-closed', 'channel-settled'])
2608
+ if (items[0]?.type !== 'session-closed' || items[1]?.type !== 'channel-settled') return
2609
+ expect(items[0].data.refund).toBeUndefined()
2610
+ expect(items[1].data.sourceToken.address).toBe(tokenA)
2611
+ })
2612
+
2613
+ test('keeps an order placement beside a swap in one batch', async () => {
2614
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2615
+ const tokenC = '0x6666666666666666666666666666666666666666' as Address.Address
2616
+ const items = await classify([
2617
+ transferRow({ from: viewer, logIdx: 0, to: stablecoinDex, token: tokenA, value: 500n }),
2618
+ logRow({
2619
+ address: stablecoinDex,
2620
+ args: {
2621
+ amount: 500n,
2622
+ flipTick: 0,
2623
+ isBid: true,
2624
+ isFlipOrder: false,
2625
+ maker: viewer,
2626
+ orderId: 7n,
2627
+ tick: 3,
2628
+ token: tokenA,
2629
+ },
2630
+ eventName: 'OrderPlaced',
2631
+ logIdx: 1,
2632
+ }),
2633
+ transferRow({ from: viewer, logIdx: 2, to: stablecoinDex, token: tokenB, value: 1_000n }),
2634
+ transferRow({ from: stablecoinDex, logIdx: 3, to: viewer, token: tokenC, value: 990n }),
2635
+ ])
2636
+ expect(items.map((item) => item.type)).toEqual(['order-placed', 'swap'])
2637
+ if (items[1]?.type !== 'swap') return
2638
+ expect(items[1].data.sourceToken).toEqual({ address: tokenB, amount: '1000' })
2639
+ })
2640
+
2641
+ test("keeps the viewer's own fill beside a third-party swap", async () => {
2642
+ const other = '0x9999999999999999999999999999999999999999' as Address.Address
2643
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2644
+ const items = await classify([
2645
+ transferRow({ from: other, logIdx: 0, to: stablecoinDex, token: tokenA, value: 1_000n }),
2646
+ logRow({
2647
+ address: stablecoinDex,
2648
+ args: {
2649
+ amountFilled: 1_000n,
2650
+ maker: viewer,
2651
+ orderId: 7n,
2652
+ partialFill: false,
2653
+ taker: other,
2654
+ },
2655
+ eventName: 'OrderFilled',
2656
+ logIdx: 1,
2657
+ }),
2658
+ transferRow({ from: stablecoinDex, logIdx: 2, to: other, token: tokenB, value: 990n }),
2659
+ ])
2660
+ expect(items.map((item) => item.type)).toEqual(['swap', 'order-filled'])
2661
+ })
2662
+
2663
+ test('folds the amount-matched vault event into a private Earn deposit', async () => {
2664
+ const actionId = `0x${'c'.repeat(64)}` as Hex.Hex
2665
+ const items = await classify([
2666
+ logRow({
2667
+ address: tokenA,
2668
+ args: { assets: 50n, caller: viewer, earnShares: 45n, receiver: viewer },
2669
+ eventName: 'Deposited',
2670
+ logIdx: 0,
2671
+ }),
2672
+ logRow({
2673
+ address: tokenA,
2674
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2675
+ eventName: 'Deposited',
2676
+ logIdx: 1,
2677
+ }),
2678
+ logRow({
2679
+ address: tokenA,
2680
+ args: {
2681
+ actionId,
2682
+ earnShares: 90n,
2683
+ earnVault: tokenA,
2684
+ inputAmount: 100n,
2685
+ inputToken: tokenA,
2686
+ vaultAssets: 100n,
2687
+ zoneDepositHash: actionId,
2688
+ },
2689
+ eventName: 'EarnDeposit',
2690
+ logIdx: 2,
2691
+ }),
2692
+ ])
2693
+ expect(items.map((item) => item.type)).toEqual(['assets-deposited', 'private-assets-deposited'])
2694
+ expect(items[0]?.type === 'assets-deposited' && items[0].data.assets).toBe('50')
2695
+ })
2696
+
2697
+ test('surfaces a fold-only vault operation when nothing else classified', async () => {
2698
+ const other = '0x9999999999999999999999999999999999999999' as Address.Address
2699
+ const asset = '0x4444444444444444444444444444444444444444' as Address.Address
2700
+ const zero = '0x0000000000000000000000000000000000000000' as Address.Address
2701
+ const items = await classify([
2702
+ transferRow({ from: other, logIdx: 0, to: zero, token: tokenA, value: 90n }),
2703
+ transferRow({ from: tokenA, logIdx: 1, to: other, token: asset, value: 100n }),
2704
+ logRow({
2705
+ address: tokenA,
2706
+ args: { assets: 100n, owner: other, receiver: other, sender: other, shares: 90n },
2707
+ eventName: 'Withdraw',
2708
+ logIdx: 2,
2709
+ }),
2710
+ ])
2711
+ expect(items.map((item) => item.type)).toEqual(['assets-withdrawn'])
2712
+ })
2713
+
2714
+ test('releases claimed legs when an anchor is partially malformed', async () => {
2715
+ const asset = '0x4444444444444444444444444444444444444444' as Address.Address
2716
+ const corrupted = logRow({
2717
+ address: tokenA,
2718
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2719
+ eventName: 'Deposited',
2720
+ logIdx: 1,
2721
+ })
2722
+ const items = await classify([
2723
+ transferRow({ from: viewer, logIdx: 0, to: tokenA, token: asset, value: 100n }),
2724
+ // Valid amounts with the indexed parties stripped: the builder throws
2725
+ // after claiming the asset leg, which must be released.
2726
+ { ...corrupted, topic1: null, topic2: null },
2727
+ ])
2728
+ expect(items.map((item) => item.type)).toEqual(['transfer'])
2729
+ })
2730
+
2731
+ test('keeps a cross-token payment beside a same-amount memo payment', async () => {
2732
+ const recipient = '0x7777777777777777777777777777777777777777' as Address.Address
2733
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2734
+ const items = await classify([
2735
+ transferRow({ from: viewer, logIdx: 0, to: recipient, token: tokenB, value: 10n }),
2736
+ logRow({
2737
+ address: tokenA,
2738
+ args: { amount: 10n, from: viewer, memo: `0x${'1'.repeat(64)}`, to: recipient },
2739
+ eventName: 'TransferWithMemo',
2740
+ logIdx: 1,
2741
+ }),
2742
+ ])
2743
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'transfer'])
2744
+ })
2745
+
2746
+ test('keeps chained swaps through two routes distinct', async () => {
2747
+ const routeA = '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f' as Address.Address
2748
+ const routeB = '0xcafe000000000000000000000000000000000001' as Address.Address
2749
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2750
+ const tokenC = '0x6666666666666666666666666666666666666666' as Address.Address
2751
+ const routeLog = (route: Address.Address, logIdx: number): Activities.Row => ({
2752
+ ...transferRow({ from: viewer, logIdx, to: route, token: tokenA, value: 0n }),
2753
+ data: '0x',
2754
+ logAddress: route,
2755
+ selector: `0x${'9'.repeat(64)}`,
2756
+ topic1: null,
2757
+ topic2: null,
2758
+ })
2759
+ const items = await classify([
2760
+ transferRow({ from: viewer, logIdx: 0, to: routeA, token: tokenA, value: 1_000n }),
2761
+ routeLog(routeA, 1),
2762
+ transferRow({ from: routeA, logIdx: 2, to: viewer, token: tokenB, value: 990n }),
2763
+ transferRow({ from: viewer, logIdx: 3, to: routeB, token: tokenB, value: 990n }),
2764
+ routeLog(routeB, 4),
2765
+ transferRow({ from: routeB, logIdx: 5, to: viewer, token: tokenC, value: 980n }),
2766
+ ])
2767
+ expect(items.map((item) => item.type)).toEqual(['swap', 'swap'])
2768
+ })
2769
+
2770
+ test('keeps an approval to an unrelated spender beside a swap', async () => {
2771
+ const spender = '0x7777777777777777777777777777777777777777' as Address.Address
2772
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2773
+ const gap: Activities.Row = {
2774
+ ...transferRow({ from: viewer, logIdx: 1, to: spender, token: tokenA, value: 0n }),
2775
+ data: '0x',
2776
+ logAddress: accountKeychain,
2777
+ selector: `0x${'8'.repeat(64)}`,
2778
+ topic1: null,
2779
+ topic2: null,
2780
+ }
2781
+ const items = await classify([
2782
+ logRow({
2783
+ address: tokenA,
2784
+ args: { amount: 1_000n, owner: viewer, spender },
2785
+ eventName: 'Approval',
2786
+ logIdx: 0,
2787
+ }),
2788
+ gap,
2789
+ transferRow({ from: viewer, logIdx: 2, to: stablecoinDex, token: tokenA, value: 1_000n }),
2790
+ transferRow({ from: stablecoinDex, logIdx: 3, to: viewer, token: tokenB, value: 990n }),
2791
+ ])
2792
+ expect(items.map((item) => item.type)).toEqual(['approval', 'swap'])
2793
+ })
2794
+
2795
+ test('keeps a settlement leg away from a zero-value channel close', async () => {
2796
+ const payee = viewer
2797
+ const channelA = `0x${'a'.repeat(64)}` as Hex.Hex
2798
+ const channelB = `0x${'b'.repeat(64)}` as Hex.Hex
2799
+ const items = await classify([
2800
+ logRow({
2801
+ address: channelReserve,
2802
+ args: {
2803
+ channelId: channelA,
2804
+ payee,
2805
+ payer: viewer,
2806
+ refundedToPayer: 0n,
2807
+ settledToPayee: 0n,
2808
+ },
2809
+ eventName: 'ChannelClosed',
2810
+ logIdx: 0,
2811
+ }),
2812
+ transferRow({ from: channelReserve, logIdx: 1, to: payee, token: tokenA, value: 250n }),
2813
+ logRow({
2814
+ address: channelReserve,
2815
+ args: {
2816
+ channelId: channelB,
2817
+ cumulativeAmount: 250n,
2818
+ deltaPaid: 250n,
2819
+ newSettled: 250n,
2820
+ payee,
2821
+ payer: viewer,
2822
+ },
2823
+ eventName: 'Settled',
2824
+ logIdx: 2,
2825
+ }),
2826
+ ])
2827
+ expect(items.map((item) => item.type)).toEqual(['channel-closed', 'channel-settled'])
2828
+ if (items[1]?.type !== 'channel-settled') return
2829
+ expect(items[1].data.sourceToken.address).toBe(tokenA)
2830
+ })
2831
+
2832
+ test('keeps an inbound fee-manager payment beside a fee distribution', async () => {
2833
+ const validator = '0xcccccccc00000000000000000000000000000000' as Address.Address
2834
+ const items = await classify([
2835
+ transferRow({ from: feeManager, logIdx: 0, to: viewer, token: tokenA, value: 77n }),
2836
+ transferRow({ from: feeManager, logIdx: 1, to: validator, token: tokenA, value: 77n }),
2837
+ logRow({
2838
+ address: feeManager,
2839
+ args: { amount: 77n, token: tokenA, validator },
2840
+ eventName: 'FeesDistributed',
2841
+ logIdx: 2,
2842
+ }),
2843
+ ])
2844
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'fees-distributed'])
2845
+ })
2846
+
2847
+ test('keeps a same-amount payment beside a reward funding', async () => {
2848
+ const other = '0x7777777777777777777777777777777777777777' as Address.Address
2849
+ const items = await classify([
2850
+ transferRow({ from: viewer, logIdx: 0, to: other, token: tokenA, value: 77n }),
2851
+ transferRow({ from: viewer, logIdx: 1, to: tokenA, token: tokenA, value: 77n }),
2852
+ logRow({
2853
+ address: tokenA,
2854
+ args: { amount: 77n, funder: viewer },
2855
+ eventName: 'RewardDistributed',
2856
+ logIdx: 2,
2857
+ }),
2858
+ ])
2859
+ expect(items.map((item) => item.type)).toEqual(['transfer', 'reward-distributed'])
2860
+ })
2861
+
2862
+ test('pairs an order with its own escrow after a same-token swap', async () => {
2863
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2864
+ const items = await classify([
2865
+ transferRow({ from: viewer, logIdx: 0, to: stablecoinDex, token: tokenA, value: 500n }),
2866
+ transferRow({ from: stablecoinDex, logIdx: 1, to: viewer, token: tokenB, value: 495n }),
2867
+ transferRow({ from: viewer, logIdx: 2, to: stablecoinDex, token: tokenA, value: 500n }),
2868
+ logRow({
2869
+ address: stablecoinDex,
2870
+ args: {
2871
+ amount: 500n,
2872
+ flipTick: 0,
2873
+ isBid: true,
2874
+ isFlipOrder: false,
2875
+ maker: viewer,
2876
+ orderId: 7n,
2877
+ tick: 3,
2878
+ token: tokenA,
2879
+ },
2880
+ eventName: 'OrderPlaced',
2881
+ logIdx: 3,
2882
+ }),
2883
+ ])
2884
+ expect(items.map((item) => item.type)).toEqual(['swap', 'order-placed'])
2885
+ })
2886
+
2887
+ test('pairs interleaved DEX swaps with their own outputs', async () => {
2888
+ const other = '0x9999999999999999999999999999999999999999' as Address.Address
2889
+ const tokenB = '0x5555555555555555555555555555555555555555' as Address.Address
2890
+ const items = await classify([
2891
+ transferRow({ from: other, logIdx: 0, to: stablecoinDex, token: tokenA, value: 1_000n }),
2892
+ transferRow({ from: viewer, logIdx: 1, to: stablecoinDex, token: tokenA, value: 500n }),
2893
+ transferRow({ from: stablecoinDex, logIdx: 2, to: viewer, token: tokenB, value: 495n }),
2894
+ transferRow({ from: stablecoinDex, logIdx: 3, to: other, token: tokenB, value: 990n }),
2895
+ ])
2896
+ expect(items.map((item) => item.type)).toEqual(['swap', 'swap'])
2897
+ if (items[0]?.type !== 'swap' || items[1]?.type !== 'swap') return
2898
+ expect(items[0].data.destinationToken.amount).toBe('990')
2899
+ expect(items[1].data.destinationToken.amount).toBe('495')
2900
+ })
2901
+
2902
+ test('keeps an unrelated-token approval to the vault beside a deposit', async () => {
2903
+ const asset = '0x4444444444444444444444444444444444444444' as Address.Address
2904
+ const tokenC = '0x6666666666666666666666666666666666666666' as Address.Address
2905
+ const zero = '0x0000000000000000000000000000000000000000' as Address.Address
2906
+ const items = await classify([
2907
+ logRow({
2908
+ address: tokenC,
2909
+ args: { amount: 100n, owner: viewer, spender: tokenA },
2910
+ eventName: 'Approval',
2911
+ logIdx: 0,
2912
+ }),
2913
+ transferRow({ from: viewer, logIdx: 1, to: tokenA, token: asset, value: 100n }),
2914
+ transferRow({ from: zero, logIdx: 2, to: viewer, token: tokenA, value: 90n }),
2915
+ logRow({
2916
+ address: tokenA,
2917
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2918
+ eventName: 'Deposited',
2919
+ logIdx: 3,
2920
+ }),
2921
+ ])
2922
+ expect(items.map((item) => item.type)).toEqual(['approval', 'assets-deposited'])
2923
+ })
2924
+
2925
+ test('folds the nearest preceding vault event into a private Earn deposit', async () => {
2926
+ const actionId = `0x${'c'.repeat(64)}` as Hex.Hex
2927
+ const items = await classify([
2928
+ logRow({
2929
+ address: tokenA,
2930
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2931
+ eventName: 'Deposited',
2932
+ logIdx: 0,
2933
+ }),
2934
+ logRow({
2935
+ address: tokenA,
2936
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2937
+ eventName: 'Deposited',
2938
+ logIdx: 1,
2939
+ }),
2940
+ logRow({
2941
+ address: tokenA,
2942
+ args: {
2943
+ actionId,
2944
+ earnShares: 90n,
2945
+ earnVault: tokenA,
2946
+ inputAmount: 100n,
2947
+ inputToken: tokenA,
2948
+ vaultAssets: 100n,
2949
+ zoneDepositHash: actionId,
2950
+ },
2951
+ eventName: 'EarnDeposit',
2952
+ logIdx: 2,
2953
+ }),
2954
+ ])
2955
+ expect(items.map((item) => item.type)).toEqual(['assets-deposited', 'private-assets-deposited'])
2956
+ expect(items[0]?.id.endsWith('-0')).toBe(true)
2957
+ })
2958
+
2959
+ test('keeps earlier activities when a later event is malformed', async () => {
2960
+ const recipient = '0x7777777777777777777777777777777777777777' as Address.Address
2961
+ const corrupted = logRow({
2962
+ address: tokenA,
2963
+ args: { assets: 100n, caller: viewer, earnShares: 90n, receiver: viewer },
2964
+ eventName: 'Deposited',
2965
+ logIdx: 1,
2966
+ })
2967
+ const items = await classify([
2968
+ transferRow({ from: viewer, logIdx: 0, to: recipient, token: tokenA, value: 10n }),
2969
+ // Valid topics with truncated data: the event decodes but its builder
2970
+ // cannot read the amounts, which must not discard the whole transaction.
2971
+ { ...corrupted, data: '0x' },
2972
+ ])
2973
+ expect(items.map((item) => item.type)).toEqual(['transfer'])
2974
+ })
2975
+ })
2976
+
2977
+ /**
2978
+ * Replays real Tempo mainnet receipts (raw logs, topics, and data) through the
2979
+ * classifier, covering batch transactions whose activities the old headline
2980
+ * classifier collapsed or misread.
2981
+ */
2982
+ describe('Activities.list receipt replays', () => {
2983
+ const viewer = '0x03ef854275a0f914a0f34db6756a59f20c821989' as Address.Address
2984
+ const txHash = `0x${'d'.repeat(64)}` as Hex.Hex
2985
+
2986
+ function store() {
2987
+ const store = Store.memory()
2988
+ void store.put('mpp-services:v1', JSON.stringify({}))
2989
+ return store
2990
+ }
2991
+
2992
+ type ReceiptLog = {
2993
+ address: string
2994
+ data: string
2995
+ logIdx: number
2996
+ topics: readonly string[]
2997
+ }
2998
+
2999
+ /** Maps a raw receipt log to a Q1 row, viewer as the transaction sender. */
3000
+ function receiptRow(log: ReceiptLog): Activities.Row {
3001
+ return {
3002
+ blockNum: 100,
3003
+ blockTimestamp: 1_700_000_000,
3004
+ data: log.data,
3005
+ feePayer: null,
3006
+ logAddress: log.address as Address.Address,
3007
+ logIdx: log.logIdx,
3008
+ selector: log.topics[0] ?? null,
3009
+ source: 'q1',
3010
+ topic1: log.topics[1] ?? null,
3011
+ topic2: log.topics[2] ?? null,
3012
+ topic3: log.topics[3] ?? null,
3013
+ txHash,
3014
+ txIdx: 0,
3015
+ txSender: viewer,
3016
+ }
3017
+ }
3018
+
3019
+ async function classify(logs: readonly ReceiptLog[]) {
3020
+ const provider: Activities.Provider = {
3021
+ cursorForGroup: (group) => `${group.blockNum}:${group.txIdx}`,
3022
+ fetchRows: async () => ({ rows: logs.map(receiptRow) }),
3023
+ }
3024
+ const { items } = await Activities.list(provider, { address: viewer, store: store() })
3025
+ return items
3026
+ }
3027
+
3028
+ const routerSwapLogs = [
3029
+ {
3030
+ address: '0x20c000000000000000000000b9537d11c60e8b50',
3031
+ data: '0x000000000000000000000000000000000000000000000000000000000010dc68',
3032
+ logIdx: 0,
3033
+ topics: [
3034
+ '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
3035
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3036
+ '0x000000000000000000000000ccc88a9d1b4ed6b0eaba998850414b24f1c315be',
3037
+ ],
3038
+ },
3039
+ {
3040
+ address: '0x20c000000000000000000000b9537d11c60e8b50',
3041
+ data: '0x000000000000000000000000000000000000000000000000000000000010dc68',
3042
+ logIdx: 1,
3043
+ topics: [
3044
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3045
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3046
+ '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3047
+ ],
3048
+ },
3049
+ {
3050
+ address: '0xccc88a9d1b4ed6b0eaba998850414b24f1c315be',
3051
+ data: '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000020c000000000000000000000b9537d11c60e8b50000000000000000000000000000000000000000000000000000000000010dc6800000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000217eb7a8dfb83149821d35b62fbd7880e2fe6e29f04133156c1adb203c95d7e2a40000000000000000000000000000000000000000000000000000000000000000',
3052
+ logIdx: 2,
3053
+ topics: ['0xafbab204e8271965231d37baed9b1abca8725b7409c70314455f68bc89142b91'],
3054
+ },
3055
+ {
3056
+ address: '0x20c000000000000000000000b9537d11c60e8b50',
3057
+ data: '0x000000000000000000000000000000000000000000000000000000000010dc68',
3058
+ logIdx: 3,
3059
+ topics: [
3060
+ '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925',
3061
+ '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3062
+ '0x0000000000000000000000000000000000001ff3684f28c67538d4d072c22734',
3063
+ ],
3064
+ },
3065
+ {
3066
+ address: '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3067
+ data: '0x00000000000000000000000020c000000000000000000000b9537d11c60e8b50000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044095ea7b30000000000000000000000000000000000001ff3684f28c67538d4d072c22734000000000000000000000000000000000000000000000000000000000010dc6800000000000000000000000000000000000000000000000000000000',
3068
+ logIdx: 4,
3069
+ topics: ['0x93485dcd31a905e3ffd7b012abe3438fa8fa77f98ddc9f50e879d3fa7ccdc324'],
3070
+ },
3071
+ {
3072
+ address: '0x20c000000000000000000000b9537d11c60e8b50',
3073
+ data: '0x00000000000000000000000000000000000000000000000000000000000003ab',
3074
+ logIdx: 5,
3075
+ topics: [
3076
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3077
+ '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3078
+ '0x000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef',
3079
+ ],
3080
+ },
3081
+ {
3082
+ address: '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3083
+ data: '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef00000000000000000000000020c000000000000000000000b9537d11c60e8b5000000000000000000000000000000000000000000000000000000000000003ab00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000217eb7a8dfb83149821d35b62fbd7880e2fe6e29f04133156c1adb203c95d7e2a40100000000000000000000000000000000000000000000000000000000000000',
3084
+ logIdx: 6,
3085
+ topics: ['0xafbab204e8271965231d37baed9b1abca8725b7409c70314455f68bc89142b91'],
3086
+ },
3087
+ {
3088
+ address: '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3089
+ data: '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a49bb43718000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000020c000000000000000000000b9537d11c60e8b500000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f70da97812cb96acdf810712aa562db8dfa3dbef000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000003ab00000000000000000000000000000000000000000000000000000000000000217eb7a8dfb83149821d35b62fbd7880e2fe6e29f04133156c1adb203c95d7e2a4010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
3090
+ logIdx: 7,
3091
+ topics: ['0x93485dcd31a905e3ffd7b012abe3438fa8fa77f98ddc9f50e879d3fa7ccdc324'],
3092
+ },
3093
+ {
3094
+ address: '0x33620f62c5b9b2086dd6b62f4a297a9f30347029',
3095
+ data: '0x000000000000000000000000000000000000000000000000000000000010d8dcffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef27430000000000000000000000000000000000000000fffbc94d34a6b108b32b149a00000000000000000000000000000000000000000000000000001d36428c070cfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0000000000000000000000000000000000000000000000000000000000000064',
3096
+ logIdx: 8,
3097
+ topics: [
3098
+ '0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f',
3099
+ '0xbd170b4ccb7f5da2565e00d5bbfa4777187b20ffb824412766463c162a47d78a',
3100
+ '0x000000000000000000000000e59f2f6ace8b9f985900f14462fbaa40385ce441',
3101
+ ],
3102
+ },
3103
+ {
3104
+ address: '0x20c0000000000000000000000000000000000000',
3105
+ data: '0x000000000000000000000000000000000000000000000000000000000010d8dc',
3106
+ logIdx: 9,
3107
+ topics: [
3108
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3109
+ '0x00000000000000000000000033620f62c5b9b2086dd6b62f4a297a9f30347029',
3110
+ '0x000000000000000000000000e59f2f6ace8b9f985900f14462fbaa40385ce441',
3111
+ ],
3112
+ },
3113
+ {
3114
+ address: '0x20c000000000000000000000b9537d11c60e8b50',
3115
+ data: '0x000000000000000000000000000000000000000000000000000000000010d8bd',
3116
+ logIdx: 10,
3117
+ topics: [
3118
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3119
+ '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3120
+ '0x00000000000000000000000033620f62c5b9b2086dd6b62f4a297a9f30347029',
3121
+ ],
3122
+ },
3123
+ {
3124
+ address: '0x20c0000000000000000000000000000000000000',
3125
+ data: '0x000000000000000000000000000000000000000000000000000000000010d8dc',
3126
+ logIdx: 11,
3127
+ topics: [
3128
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3129
+ '0x000000000000000000000000e59f2f6ace8b9f985900f14462fbaa40385ce441',
3130
+ '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3131
+ ],
3132
+ },
3133
+ {
3134
+ address: '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3135
+ data: '0x0000000000000000000000000000000000001ff3684f28c67538d4d072c227340000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004e42213bc0b000000000000000000000000e59f2f6ace8b9f985900f14462fbaa40385ce44100000000000000000000000020c000000000000000000000b9537d11c60e8b50000000000000000000000000000000000000000000000000000000000010d8bd000000000000000000000000e59f2f6ace8b9f985900f14462fbaa40385ce44100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000004041fff991f000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000020c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010c34a00000000000000000000000000000000000000000000000000000000000000a0d70e27de7eb4ff3a242a1d170af43c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000002800000000000000000000000000000000000000000000000000000000000000204931997d3000000000000000000000000e59f2f6ace8b9f985900f14462fbaa40385ce44100000000000000000000000020c000000000000000000000b9537d11c60e8b50000000000000000000000000000000000000000000000000000000000010d8bd0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006a862ef800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000ffffffffffffffc5000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000482710fffd8963efd1fc6a506488495d951d5263988d250120c00000000000000000000000000000000000000000640000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008434ee90ca000000000000000000000000f5c4f3dc02c3fb9279495a8fef7b0741da95615700000000000000000000000020c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010d98b0000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
3136
+ logIdx: 12,
3137
+ topics: ['0x93485dcd31a905e3ffd7b012abe3438fa8fa77f98ddc9f50e879d3fa7ccdc324'],
3138
+ },
3139
+ {
3140
+ address: '0x20c0000000000000000000000000000000000000',
3141
+ data: '0x000000000000000000000000000000000000000000000000000000000010d8dc',
3142
+ logIdx: 13,
3143
+ topics: [
3144
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3145
+ '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3146
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3147
+ ],
3148
+ },
3149
+ {
3150
+ address: '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3151
+ data: '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f00000000000000000000000003ef854275a0f914a0f34db6756a59f20c82198900000000000000000000000020c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010d8dc00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000217eb7a8dfb83149821d35b62fbd7880e2fe6e29f04133156c1adb203c95d7e2a40000000000000000000000000000000000000000000000000000000000000000',
3152
+ logIdx: 14,
3153
+ topics: ['0xafbab204e8271965231d37baed9b1abca8725b7409c70314455f68bc89142b91'],
3154
+ },
3155
+ {
3156
+ address: '0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f',
3157
+ data: '0x000000000000000000000000b92fe925dc43a0ecde6c8b1a2709c170ec4fff4f0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a49bb43718000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000020c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000003ef854275a0f914a0f34db6756a59f20c8219890000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000217eb7a8dfb83149821d35b62fbd7880e2fe6e29f04133156c1adb203c95d7e2a4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
3158
+ logIdx: 15,
3159
+ topics: ['0x93485dcd31a905e3ffd7b012abe3438fa8fa77f98ddc9f50e879d3fa7ccdc324'],
3160
+ },
3161
+ {
3162
+ address: '0x20c0000000000000000000000000000000000000',
3163
+ data: '0x00000000000000000000000000000000000000000000000000000000000000ab',
3164
+ logIdx: 16,
3165
+ topics: [
3166
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3167
+ '0x00000000000000000000000058aa7ce42e1d13b2919e2ac7e006c4fbc171442c',
3168
+ '0x000000000000000000000000feec000000000000000000000000000000000000',
3169
+ ],
3170
+ },
3171
+ ]
3172
+
3173
+ const vaultWithdrawLogs = [
3174
+ {
3175
+ address: '0x0ec811f25e8fc247fe98faf32b90a48683d62f05',
3176
+ data: '0x00000000000000000000000000000000000000000000000000000000156963bb00000000000000000000000000000000000000000000000000000000156963bb00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
3177
+ logIdx: 0,
3178
+ topics: ['0x4dec04e750ca11537cabcd8a9eab06494de08da3735bc8871cd41250e190bc04'],
3179
+ },
3180
+ {
3181
+ address: '0x9a044ae05e5e6290dcf56afd69548565e957a626',
3182
+ data: '0x000000000000000000000000000000000000000000000000000014bc099ff380000000000000000000000000000000000000000000000000000014bc09a0344000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
3183
+ logIdx: 1,
3184
+ topics: ['0x4dec04e750ca11537cabcd8a9eab06494de08da3735bc8871cd41250e190bc04'],
3185
+ },
3186
+ {
3187
+ address: '0x9a044ae05e5e6290dcf56afd69548565e957a626',
3188
+ data: '0x00000000000000000000000000000000000000000000000000b1bcfa85011524',
3189
+ logIdx: 2,
3190
+ topics: [
3191
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3192
+ '0x000000000000000000000000004ad9220bce494c1f1892ee4965972fde188d51',
3193
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
3194
+ ],
3195
+ },
3196
+ {
3197
+ address: '0x20c0000000000000000000000000000000000000',
3198
+ data: '0x00000000000000000000000000000000000000000000000000000000000186a0',
3199
+ logIdx: 3,
3200
+ topics: [
3201
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3202
+ '0x0000000000000000000000009a044ae05e5e6290dcf56afd69548565e957a626',
3203
+ '0x000000000000000000000000004ad9220bce494c1f1892ee4965972fde188d51',
3204
+ ],
3205
+ },
3206
+ {
3207
+ address: '0x9a044ae05e5e6290dcf56afd69548565e957a626',
3208
+ data: '0x00000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000000b1bcfa85011524',
3209
+ logIdx: 4,
3210
+ topics: [
3211
+ '0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db',
3212
+ '0x000000000000000000000000004ad9220bce494c1f1892ee4965972fde188d51',
3213
+ '0x000000000000000000000000004ad9220bce494c1f1892ee4965972fde188d51',
3214
+ '0x000000000000000000000000004ad9220bce494c1f1892ee4965972fde188d51',
3215
+ ],
3216
+ },
3217
+ {
3218
+ address: '0x20c0000000000000000000000000000000000000',
3219
+ data: '0x00000000000000000000000000000000000000000000000000000000000186a0',
3220
+ logIdx: 5,
3221
+ topics: [
3222
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3223
+ '0x000000000000000000000000004ad9220bce494c1f1892ee4965972fde188d51',
3224
+ '0x0000000000000000000000000ec811f25e8fc247fe98faf32b90a48683d62f05',
3225
+ ],
3226
+ },
3227
+ {
3228
+ address: '0x0ec811f25e8fc247fe98faf32b90a48683d62f05',
3229
+ data: '0x00000000000000000000000000000000000000000000000000000000000186a00000000000000000000000000000000000000000000000000000000000000060fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe79600000000000000000000000000000000000000000000000000000000000000001cad5f244418b485269ca822a1e213ac83a6fed12d0c16e0741eb94e4545b05e8',
3230
+ logIdx: 6,
3231
+ topics: [
3232
+ '0xd602b36fb24934aef1bc2a658de029b486fa4c664a6e45de1f48e3fd1be25dd9',
3233
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3234
+ '0x000000000000000000000000004ad9220bce494c1f1892ee4965972fde188d51',
3235
+ ],
3236
+ },
3237
+ {
3238
+ address: '0x0ec811f25e8fc247fe98faf32b90a48683d62f05',
3239
+ data: '0x00000000000000000000000000000000000000000000000001634373939b29b4',
3240
+ logIdx: 7,
3241
+ topics: [
3242
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3243
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3244
+ '0x0000000000000000000000000000000000000000000000000000000000000000',
3245
+ ],
3246
+ },
3247
+ {
3248
+ address: '0x20c0000000000000000000000000000000000000',
3249
+ data: '0x00000000000000000000000000000000000000000000000000000000000186a0',
3250
+ logIdx: 8,
3251
+ topics: [
3252
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3253
+ '0x0000000000000000000000000ec811f25e8fc247fe98faf32b90a48683d62f05',
3254
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3255
+ ],
3256
+ },
3257
+ {
3258
+ address: '0x0ec811f25e8fc247fe98faf32b90a48683d62f05',
3259
+ data: '0x00000000000000000000000000000000000000000000000000000000000186a000000000000000000000000000000000000000000000000001634373939b29b4',
3260
+ logIdx: 9,
3261
+ topics: [
3262
+ '0xfbde797d201c681b91056529119e0b02407c7bb96a4a2c75c01fc9667232c8db',
3263
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3264
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3265
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3266
+ ],
3267
+ },
3268
+ {
3269
+ address: '0x20c0000000000000000000000000000000000000',
3270
+ data: '0x00000000000000000000000000000000000000000000000000000000000186a0',
3271
+ logIdx: 10,
3272
+ topics: [
3273
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3274
+ '0x00000000000000000000000003ef854275a0f914a0f34db6756a59f20c821989',
3275
+ '0x00000000000000000000000076a21038ea44d8942c0765175f05d34fcfb838ab',
3276
+ ],
3277
+ },
3278
+ {
3279
+ address: '0x20c0000000000000000000000000000000000000',
3280
+ data: '0x0000000000000000000000000000000000000000000000000000000000000096',
3281
+ logIdx: 11,
3282
+ topics: [
3283
+ '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
3284
+ '0x00000000000000000000000058aa7ce42e1d13b2919e2ac7e006c4fbc171442c',
3285
+ '0x000000000000000000000000feec000000000000000000000000000000000000',
3286
+ ],
3287
+ },
3288
+ ]
3289
+
3290
+ test('classifies a router swap instead of a lonely approval', async () => {
3291
+ // 0xd8be4bfb370d4395c45fb587686b0bd83b01228871896926e92d66d15f25838b
3292
+ // (chain 4217): 1.105 USDC.e swapped to pathUSD through a route account.
3293
+ const items = await classify(routerSwapLogs)
3294
+ expect(items.map((item) => item.type)).toEqual(['swap'])
3295
+ const swap = items[0]
3296
+ if (swap?.type !== 'swap') return
3297
+ expect(swap.data.sourceToken).toEqual({
3298
+ address: '0x20c000000000000000000000b9537d11c60e8b50',
3299
+ amount: '1105000',
3300
+ })
3301
+ expect(swap.data.destinationToken).toEqual({
3302
+ address: '0x20c0000000000000000000000000000000000000',
3303
+ amount: '1104092',
3304
+ })
3305
+ })
3306
+
3307
+ test('classifies a batched vault withdrawal and payment separately', async () => {
3308
+ // 0xf438783016504c10273a74631730fef8bfd5209a655c3e1ea7a87848d6406a7c
3309
+ // (chain 4217): ERC-4626 withdraw of 0.10 pathUSD, then a 0.10 pathUSD
3310
+ // payment. The old classifier reported a single `burn`.
3311
+ const items = await classify(vaultWithdrawLogs)
3312
+ expect(items.map((item) => item.type)).toEqual(['assets-withdrawn', 'transfer'])
3313
+ const withdrawal = items[0]
3314
+ if (withdrawal?.type !== 'assets-withdrawn') return
3315
+ expect(withdrawal.data.vault).toBe('0x0ec811f25e8fc247fe98faf32b90a48683d62f05')
3316
+ expect(withdrawal.data.assets).toBe('100000')
3317
+ expect(withdrawal.data.receiver).toBe(viewer)
3318
+ const payment = items[1]
3319
+ if (payment?.type !== 'transfer') return
3320
+ expect(payment.data.direction).toBe('out')
3321
+ expect(payment.data.recipient).toBe('0x76a21038ea44d8942c0765175f05d34fcfb838ab')
3322
+ expect(payment.data.sourceToken).toEqual({
3323
+ address: '0x20c0000000000000000000000000000000000000',
3324
+ amount: '100000',
3325
+ })
3326
+ })
1486
3327
  })