tapimo 0.12.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 (257) hide show
  1. package/dist/App.d.ts +5561 -5026
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +38 -3
  4. package/dist/App.js.map +1 -1
  5. package/dist/Scope.d.ts +2 -0
  6. package/dist/Scope.d.ts.map +1 -1
  7. package/dist/Scope.js +17 -0
  8. package/dist/Scope.js.map +1 -1
  9. package/dist/admin/App.d.ts +123 -74
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/apps/api-keys.d.ts +80 -0
  12. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  13. package/dist/admin/apps/api-keys.js +54 -0
  14. package/dist/admin/apps/api-keys.js.map +1 -1
  15. package/dist/admin/apps/earn-vaults.d.ts +15 -15
  16. package/dist/admin/apps/earn-vaults.js +1 -1
  17. package/dist/admin/apps/verified-tokens.d.ts +81 -81
  18. package/dist/admin/ui.generated.d.ts +1 -1
  19. package/dist/admin/ui.generated.d.ts.map +1 -1
  20. package/dist/admin/ui.generated.js +151 -151
  21. package/dist/admin/ui.generated.js.map +1 -1
  22. package/dist/apps/Zones.d.ts +16 -16
  23. package/dist/apps/Zones.js +4 -4
  24. package/dist/apps/data/App.js +18 -18
  25. package/dist/apps/data/App.js.map +1 -1
  26. package/dist/apps/data/ZoneSelection.d.ts +8 -8
  27. package/dist/apps/data/ZoneSelection.d.ts.map +1 -1
  28. package/dist/apps/data/ZoneSelection.js +1 -1
  29. package/dist/apps/data/ZoneSelection.js.map +1 -1
  30. package/dist/apps/data/routes/activities.d.ts +588 -584
  31. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  32. package/dist/apps/data/routes/activities.js +1561 -1239
  33. package/dist/apps/data/routes/activities.js.map +1 -1
  34. package/dist/apps/data/routes/balances.d.ts +26 -26
  35. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  36. package/dist/apps/data/routes/balances.js +43 -16
  37. package/dist/apps/data/routes/balances.js.map +1 -1
  38. package/dist/apps/data/routes/blocks.d.ts +44 -44
  39. package/dist/apps/data/routes/blocks.d.ts.map +1 -1
  40. package/dist/apps/data/routes/blocks.js +21 -11
  41. package/dist/apps/data/routes/blocks.js.map +1 -1
  42. package/dist/apps/data/routes/coingecko.d.ts +3 -3
  43. package/dist/apps/data/routes/earn.d.ts +18 -18
  44. package/dist/apps/data/routes/earn.js +13 -13
  45. package/dist/apps/data/routes/exchanges.d.ts +145 -145
  46. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  47. package/dist/apps/data/routes/receipts.d.ts +155 -55
  48. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  49. package/dist/apps/data/routes/receipts.js +28 -10
  50. package/dist/apps/data/routes/receipts.js.map +1 -1
  51. package/dist/apps/data/routes/tokenlist.d.ts +13 -13
  52. package/dist/apps/data/routes/tokens.d.ts +98 -98
  53. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  54. package/dist/apps/data/routes/tokens.js +51 -20
  55. package/dist/apps/data/routes/tokens.js.map +1 -1
  56. package/dist/apps/data/routes/transactions.d.ts +7 -2
  57. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  58. package/dist/apps/data/routes/transactions.js +37 -17
  59. package/dist/apps/data/routes/transactions.js.map +1 -1
  60. package/dist/apps/data/routes/transfers.d.ts +1 -1
  61. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  62. package/dist/apps/data/routes/transfers.js +18 -12
  63. package/dist/apps/data/routes/transfers.js.map +1 -1
  64. package/dist/apps/data/routes/valuation.d.ts +1 -1
  65. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  66. package/dist/apps/data/routes/valuation.js +12 -7
  67. package/dist/apps/data/routes/valuation.js.map +1 -1
  68. package/dist/apps/data/routes/verified-tokens.d.ts +3 -3
  69. package/dist/apps/data/routes/webhooks.d.ts +11 -11
  70. package/dist/apps/data/routes/zones.d.ts +14 -14
  71. package/dist/apps/data/routes/zones.js +1 -1
  72. package/dist/apps/funding/App.d.ts +11 -0
  73. package/dist/apps/funding/App.d.ts.map +1 -1
  74. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  75. package/dist/apps/funding/routes/deposit-addresses.js +16 -11
  76. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  77. package/dist/apps/funding/routes/transfers.d.ts +60 -48
  78. package/dist/apps/funding/routes/transfers.d.ts.map +1 -1
  79. package/dist/apps/funding/routes/transfers.js +27 -19
  80. package/dist/apps/funding/routes/transfers.js.map +1 -1
  81. package/dist/apps/management/App.d.ts +4487 -3963
  82. package/dist/apps/management/App.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.d.ts +710 -159
  84. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  85. package/dist/apps/management/routes/api-keys.js +236 -4
  86. package/dist/apps/management/routes/api-keys.js.map +1 -1
  87. package/dist/apps/management/routes/faucet.d.ts +6 -6
  88. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  89. package/dist/apps/management/routes/members.d.ts +30 -30
  90. package/dist/apps/management/routes/projects.d.ts +40 -40
  91. package/dist/apps/management/routes/usage.d.ts +24 -24
  92. package/dist/apps/management/routes/verified-token-requests.d.ts +1 -1
  93. package/dist/apps/management/routes/webhooks.d.ts +55 -55
  94. package/dist/db/Db.d.ts.map +1 -1
  95. package/dist/db/Db.js +12 -1
  96. package/dist/db/Db.js.map +1 -1
  97. package/dist/db/Schema.d.ts +3 -2
  98. package/dist/db/Schema.d.ts.map +1 -1
  99. package/dist/db/Schema.js +5 -2
  100. package/dist/db/Schema.js.map +1 -1
  101. package/dist/db/tables/fundingCatalog.d.ts +4 -2
  102. package/dist/db/tables/fundingCatalog.d.ts.map +1 -1
  103. package/dist/db/tables/fundingCatalog.js +5 -1
  104. package/dist/db/tables/fundingCatalog.js.map +1 -1
  105. package/dist/handlers/internal/utils.d.ts.map +1 -1
  106. package/dist/handlers/internal/utils.js +9 -0
  107. package/dist/handlers/internal/utils.js.map +1 -1
  108. package/dist/internal/Auth.d.ts +3 -1
  109. package/dist/internal/Auth.d.ts.map +1 -1
  110. package/dist/internal/Auth.js +6 -3
  111. package/dist/internal/Auth.js.map +1 -1
  112. package/dist/internal/EarnVaults.d.ts +3 -3
  113. package/dist/internal/EarnVaults.js +3 -3
  114. package/dist/internal/Fetch.d.ts +23 -0
  115. package/dist/internal/Fetch.d.ts.map +1 -0
  116. package/dist/internal/Fetch.js +26 -0
  117. package/dist/internal/Fetch.js.map +1 -0
  118. package/dist/internal/Log.d.ts +3 -1
  119. package/dist/internal/Log.d.ts.map +1 -1
  120. package/dist/internal/Log.js +16 -3
  121. package/dist/internal/Log.js.map +1 -1
  122. package/dist/internal/OpenApi.d.ts +5 -3
  123. package/dist/internal/OpenApi.d.ts.map +1 -1
  124. package/dist/internal/OpenApi.js +13 -3
  125. package/dist/internal/OpenApi.js.map +1 -1
  126. package/dist/internal/Response.d.ts +1 -1
  127. package/dist/internal/Response.d.ts.map +1 -1
  128. package/dist/internal/Schema.d.ts +6 -3
  129. package/dist/internal/Schema.d.ts.map +1 -1
  130. package/dist/internal/Schema.js +8 -2
  131. package/dist/internal/Schema.js.map +1 -1
  132. package/dist/internal/Store.d.ts +16 -3
  133. package/dist/internal/Store.d.ts.map +1 -1
  134. package/dist/internal/Store.js +82 -16
  135. package/dist/internal/Store.js.map +1 -1
  136. package/dist/internal/Tidx.d.ts.map +1 -1
  137. package/dist/internal/Tidx.js +7 -2
  138. package/dist/internal/Tidx.js.map +1 -1
  139. package/dist/internal/Timeout.d.ts +36 -0
  140. package/dist/internal/Timeout.d.ts.map +1 -0
  141. package/dist/internal/Timeout.js +37 -0
  142. package/dist/internal/Timeout.js.map +1 -0
  143. package/dist/internal/Timing.d.ts +9 -0
  144. package/dist/internal/Timing.d.ts.map +1 -1
  145. package/dist/internal/Timing.js +7 -0
  146. package/dist/internal/Timing.js.map +1 -1
  147. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  148. package/dist/internal/WebhookDestination.js +3 -0
  149. package/dist/internal/WebhookDestination.js.map +1 -1
  150. package/dist/internal/funding/Action.d.ts +9 -0
  151. package/dist/internal/funding/Action.d.ts.map +1 -1
  152. package/dist/internal/funding/Action.js +16 -8
  153. package/dist/internal/funding/Action.js.map +1 -1
  154. package/dist/internal/funding/Catalog.d.ts +9 -2
  155. package/dist/internal/funding/Catalog.d.ts.map +1 -1
  156. package/dist/internal/funding/Catalog.js +19 -8
  157. package/dist/internal/funding/Catalog.js.map +1 -1
  158. package/dist/internal/funding/Provider.d.ts +68 -13
  159. package/dist/internal/funding/Provider.d.ts.map +1 -1
  160. package/dist/internal/funding/Provider.js +29 -8
  161. package/dist/internal/funding/Provider.js.map +1 -1
  162. package/dist/internal/funding/Transfer.d.ts +13 -12
  163. package/dist/internal/funding/Transfer.d.ts.map +1 -1
  164. package/dist/internal/funding/Transfer.js +23 -21
  165. package/dist/internal/funding/Transfer.js.map +1 -1
  166. package/dist/internal/funding/providers/stargate.d.ts +28 -9
  167. package/dist/internal/funding/providers/stargate.d.ts.map +1 -1
  168. package/dist/internal/funding/providers/stargate.js +251 -117
  169. package/dist/internal/funding/providers/stargate.js.map +1 -1
  170. package/dist/internal/index.d.ts +1 -0
  171. package/dist/internal/index.d.ts.map +1 -1
  172. package/dist/internal/index.js +1 -0
  173. package/dist/internal/index.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/App.test.ts +100 -0
  176. package/src/App.ts +42 -3
  177. package/src/Client.test-d.ts +46 -0
  178. package/src/Scope.test.ts +20 -0
  179. package/src/Scope.ts +16 -0
  180. package/src/admin/apps/api-keys.test.ts +75 -0
  181. package/src/admin/apps/api-keys.ts +64 -0
  182. package/src/admin/apps/earn-vaults.test.ts +1 -1
  183. package/src/admin/apps/earn-vaults.ts +1 -1
  184. package/src/admin/ui/src/lib/api-keys.ts +19 -0
  185. package/src/admin/ui/src/routes/api-keys.tsx +89 -2
  186. package/src/admin/ui.generated.ts +189 -151
  187. package/src/apps/Zones.test.ts +4 -4
  188. package/src/apps/Zones.ts +4 -4
  189. package/src/apps/composition.test.ts +1 -1
  190. package/src/apps/data/App.test.ts +41 -0
  191. package/src/apps/data/App.ts +18 -18
  192. package/src/apps/data/ZoneSelection.test.ts +2 -2
  193. package/src/apps/data/ZoneSelection.ts +8 -8
  194. package/src/apps/data/routes/activities.test.ts +1526 -12
  195. package/src/apps/data/routes/activities.ts +1774 -1356
  196. package/src/apps/data/routes/balances.ts +133 -97
  197. package/src/apps/data/routes/blocks.test.ts +533 -0
  198. package/src/apps/data/routes/blocks.ts +35 -16
  199. package/src/apps/data/routes/earn.test.ts +4 -2
  200. package/src/apps/data/routes/earn.ts +13 -13
  201. package/src/apps/data/routes/earn.zone-access.test.ts +6 -6
  202. package/src/apps/data/routes/receipts.ts +47 -16
  203. package/src/apps/data/routes/tokens.test.ts +578 -0
  204. package/src/apps/data/routes/tokens.ts +283 -225
  205. package/src/apps/data/routes/transactions.ts +68 -29
  206. package/src/apps/data/routes/transfers.test.ts +421 -0
  207. package/src/apps/data/routes/transfers.ts +45 -27
  208. package/src/apps/data/routes/valuation.ts +94 -78
  209. package/src/apps/data/routes/zones.test.ts +2 -2
  210. package/src/apps/data/routes/zones.ts +1 -1
  211. package/src/apps/funding/routes/deposit-addresses.test.ts +25 -3
  212. package/src/apps/funding/routes/deposit-addresses.ts +17 -12
  213. package/src/apps/funding/routes/quotes.test.ts +5 -9
  214. package/src/apps/funding/routes/transfers.test.ts +395 -8
  215. package/src/apps/funding/routes/transfers.ts +76 -59
  216. package/src/apps/management/routes/api-keys.test.ts +220 -0
  217. package/src/apps/management/routes/api-keys.ts +280 -3
  218. package/src/db/Db.ts +13 -1
  219. package/src/db/Schema.ts +5 -2
  220. package/src/db/tables/earnVaults.test.ts +9 -9
  221. package/src/db/tables/fundingCatalog.test.ts +16 -2
  222. package/src/db/tables/fundingCatalog.ts +9 -3
  223. package/src/handlers/internal/utils.test.ts +53 -0
  224. package/src/handlers/internal/utils.ts +15 -0
  225. package/src/internal/Auth.ts +12 -4
  226. package/src/internal/EarnVaults.capabilities.test.ts +2 -2
  227. package/src/internal/EarnVaults.test.ts +1 -1
  228. package/src/internal/EarnVaults.ts +3 -3
  229. package/src/internal/Fetch.test.ts +46 -0
  230. package/src/internal/Fetch.ts +56 -0
  231. package/src/internal/Log.test.ts +21 -0
  232. package/src/internal/Log.ts +20 -5
  233. package/src/internal/OpenApi.ts +14 -4
  234. package/src/internal/Response.ts +1 -1
  235. package/src/internal/Schema.test.ts +2 -0
  236. package/src/internal/Schema.ts +12 -2
  237. package/src/internal/Store.test.ts +178 -0
  238. package/src/internal/Store.ts +104 -18
  239. package/src/internal/Tidx.test.ts +2 -1
  240. package/src/internal/Tidx.ts +7 -2
  241. package/src/internal/Timeout.test.ts +35 -0
  242. package/src/internal/Timeout.ts +73 -0
  243. package/src/internal/Timing.ts +16 -0
  244. package/src/internal/WebhookDestination.ts +3 -0
  245. package/src/internal/funding/Action.test.ts +41 -1
  246. package/src/internal/funding/Action.ts +69 -41
  247. package/src/internal/funding/Catalog.test.ts +34 -0
  248. package/src/internal/funding/Catalog.ts +28 -11
  249. package/src/internal/funding/Provider.test.ts +73 -0
  250. package/src/internal/funding/Provider.ts +82 -28
  251. package/src/internal/funding/Transfer.ts +133 -110
  252. package/src/internal/funding/providers/index.test.ts +4 -167
  253. package/src/internal/funding/providers/relay.test.ts +2 -2
  254. package/src/internal/funding/providers/stargate.test.ts +163 -0
  255. package/src/internal/funding/providers/stargate.ts +359 -134
  256. package/src/internal/index.test.ts +1 -0
  257. package/src/internal/index.ts +1 -0
@@ -655,7 +655,7 @@ describe('GET /transactions/:transactionHash/activities', () => {
655
655
  account: account.address,
656
656
  token: Addresses.pathUsd,
657
657
  })
658
- await Actions.zone.depositSync(Tempo.getClient({ account }), {
658
+ const zoneDeposit = await Actions.zone.depositSync(Tempo.getClient({ account }), {
659
659
  amount: 1_000_000n,
660
660
  portalAddress: zoneCreated.args.portal,
661
661
  token: Addresses.pathUsd,
@@ -721,6 +721,48 @@ describe('GET /transactions/:transactionHash/activities', () => {
721
721
  expect(cached.status).toBe(200)
722
722
  expect(cached.headers.get('server-timing')).not.toContain('tidx;dur=')
723
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
+
724
766
  const missingHash = `0x${'ff'.repeat(32)}` as Hex.Hex
725
767
  const getMissing = () =>
726
768
  client.v1.transactions[':transactionHash'].activities.$get(
@@ -760,10 +802,14 @@ describe('GET /transactions/:transactionHash/activities', () => {
760
802
  response,
761
803
  Activities.schema.getAddressActivities.Response,
762
804
  )
805
+ const hashes = body.data.flatMap((item) =>
806
+ item.type === 'group'
807
+ ? item.data.items.map(({ transactionHash }) => transactionHash)
808
+ : [item.transactionHash],
809
+ )
763
810
  if (
764
- body.data.some(
765
- (item) => item.type !== 'group' && item.transactionHash === receipt.transactionHash,
766
- )
811
+ hashes.includes(receipt.transactionHash) &&
812
+ hashes.includes(zoneDeposit.receipt.transactionHash)
767
813
  )
768
814
  return { body, response }
769
815
  }
@@ -776,6 +822,35 @@ describe('GET /transactions/:transactionHash/activities', () => {
776
822
  )
777
823
  expect(zoneAddressActivities.response.headers.get('cache-control')).toBe('no-store')
778
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)
779
854
  },
780
855
  180_000,
781
856
  )
@@ -841,7 +916,38 @@ describe('Activities.list classification', () => {
841
916
  }
842
917
  }
843
918
 
844
- test('merges Zone pages and retains each provider cursor', async () => {
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 () => {
845
951
  const providers = [
846
952
  {
847
953
  chainId: 10,
@@ -855,13 +961,13 @@ describe('Activities.list classification', () => {
855
961
  provider: pagedProvider([unknownRow({ blockNum: 20, blockTimestamp: 200, hash: 'b' })]),
856
962
  },
857
963
  ]
858
- const first = await Activities.listZones({
964
+ const first = await Activities.listChains({
859
965
  address: viewer,
860
966
  limit: 2,
861
967
  providers,
862
968
  store: store(),
863
969
  })
864
- const second = await Activities.listZones({
970
+ const second = await Activities.listChains({
865
971
  address: viewer,
866
972
  cursor: first.nextCursor!,
867
973
  limit: 2,
@@ -878,7 +984,7 @@ describe('Activities.list classification', () => {
878
984
  expect(second.nextCursor).toBeNull()
879
985
  })
880
986
 
881
- test('retains unused Zone buffers while filling a grouped page', async () => {
987
+ test('retains unused chain buffers while filling a grouped page', async () => {
882
988
  const signer = '0x3333333333333333333333333333333333333333' as Address.Address
883
989
  const calls = new Map<number, number>()
884
990
  const providers = [
@@ -904,7 +1010,7 @@ describe('Activities.list classification', () => {
904
1010
  }
905
1011
  })
906
1012
 
907
- const result = await Activities.listZones({
1013
+ const result = await Activities.listChains({
908
1014
  address: viewer,
909
1015
  group: true,
910
1016
  limit: 2,
@@ -927,7 +1033,7 @@ describe('Activities.list classification', () => {
927
1033
  expect(result.nextCursor).toBeNull()
928
1034
  })
929
1035
 
930
- test('does not fold access-key activity across Zones', async () => {
1036
+ test('does not fold access-key activity across chains', async () => {
931
1037
  const signer = '0x3333333333333333333333333333333333333333' as Address.Address
932
1038
  const providers = [10, 20].map((chainId) => ({
933
1039
  chainId,
@@ -939,7 +1045,7 @@ describe('Activities.list classification', () => {
939
1045
  },
940
1046
  ]),
941
1047
  }))
942
- const result = await Activities.listZones({
1048
+ const result = await Activities.listChains({
943
1049
  address: viewer,
944
1050
  providers,
945
1051
  store: store(),
@@ -1122,7 +1228,31 @@ describe('Activities.list precompile classification', () => {
1122
1228
  logIdx: number
1123
1229
  }): Activities.Row {
1124
1230
  type Param = { indexed?: boolean; name?: string; type: string }
1125
- 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]
1126
1256
  const event = abi.find((a) => a.type === 'event' && a.name === options.eventName) as {
1127
1257
  inputs: readonly Param[]
1128
1258
  }
@@ -1810,4 +1940,1388 @@ describe('Activities.list precompile classification', () => {
1810
1940
  if (items[0]!.type !== 'initialized') return
1811
1941
  expect(items[0]!.data.height).toBe('42')
1812
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
+ })
1813
3327
  })