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
@@ -37,29 +37,26 @@ export var schema;
37
37
  .union([z.literal('self'), Schema.Address])
38
38
  .check(z.describe('Who signed the activity: `self`, or the access-key address used to sign it.'));
39
39
  /** A token with required RPC metadata and optional curated fields. */
40
- schema.Token = Schema.describe(Tokens.schema.TokenReference, 'A token referenced by an activity item.');
41
- /** A token amount carrying its nominal value in the requested denomination. */
42
- const ValuedAmount = z.extend(Schema.TokenAmount, {
40
+ schema.Token = OpenApi.component(Schema.describe(Tokens.schema.TokenReference, 'A token referenced by an activity item.'), 'ActivityToken');
41
+ const ValuedAmount = Schema.describe(z.extend(Schema.TokenAmount, {
43
42
  valuation: z
44
43
  .optional(z.nullable(Valuation.schema.Value))
45
44
  .check(z.describe('The amount’s nominal value in the requested `valuation.currency`. `null` when the token is ' +
46
45
  'unverified, its display currency has no rate, or rates were unavailable.')),
47
- });
48
- /** Self-contained amount for a destination token. */
49
- const DestinationAmount = ValuedAmount.check(z.describe('Amount associated with `destinationToken`.'));
50
- /** Self-contained amount for a refunded token. */
51
- const RefundAmount = ValuedAmount.check(z.describe('Amount associated with `refund`.'));
52
- /** Self-contained amount for a source token. */
53
- const SourceAmount = ValuedAmount.check(z.describe('Amount associated with `sourceToken`.'));
46
+ }), 'A token amount carrying its nominal value in the requested denomination.');
47
+ const valuedAmount = OpenApi.component(ValuedAmount, 'ActivityValuedAmount');
48
+ const DestinationAmount = valuedAmount;
49
+ const RefundAmount = valuedAmount;
50
+ const SourceAmount = valuedAmount;
54
51
  /** Page-level resources: valuation rate provenance. */
55
- schema.Meta = z
52
+ schema.Meta = OpenApi.component(z
56
53
  .object({
57
54
  valuation: z
58
55
  .optional(Valuation.schema.Pricing)
59
56
  .check(z.describe('Rate provenance for amount valuations. Present when conversion rates were ' +
60
57
  'consulted; absent when every value was identity-valued or rates were unavailable.')),
61
58
  })
62
- .check(z.describe('Page-level resources attached to this response.'));
59
+ .check(z.describe('Page-level resources attached to this response.')), 'ActivityMeta');
63
60
  /** A token transfer with subject-relative direction and signer fields. */
64
61
  schema.Transfer = z
65
62
  .object({
@@ -97,6 +94,7 @@ export var schema;
97
94
  error: '`destinationAmount` and `destinationToken` must be returned together.',
98
95
  path: ['destinationAmount'],
99
96
  }), z.describe('One token transfer involving the requested address.'));
97
+ const activityTransfer = OpenApi.component(schema.Transfer, 'ActivityTransfer');
100
98
  /**
101
99
  * One transaction log, decoded into a known Tempo event when possible. The
102
100
  * raw fields (`address`/`data`/`logIndex`/`topics`) are always present so no
@@ -124,6 +122,7 @@ export var schema;
124
122
  .check(z.describe('Raw log topics; the first topic is the event signature hash.')),
125
123
  })
126
124
  .check(z.describe('One transaction log. Decoded fields (`eventName`/`args`/`title`) are present when it matched a known Tempo ABI; otherwise only the raw fields are set.'));
125
+ const activityEvent = OpenApi.component(event, 'ActivityEvent');
127
126
  /**
128
127
  * Fields shared by every activity item. `transactionHash`/`timestamp` identify and sort
129
128
  * the entry; `data` carries the type-specific payload — shaped like the
@@ -133,12 +132,12 @@ export var schema;
133
132
  const base = {
134
133
  chainId: z
135
134
  .optional(Schema.ChainId)
136
- .check(z.describe('Chain id containing this activity. Returned for multi-Zone requests.'), z.meta({ examples: [421700001] })),
135
+ .check(z.describe('Chain ID containing this activity. Returned when `include=zones`.'), z.meta({ examples: [421700001] })),
137
136
  perspective: z
138
137
  .enum(['incoming', 'outgoing'])
139
138
  .check(z.describe('Your role in the transaction: `outgoing` when you sent or paid for it, `incoming` when it happened to you (someone else initiated it). For a `transfer`, `data.direction` is the finer-grained token-flow view.')),
140
139
  events: z
141
- .optional(z.array(event))
140
+ .optional(z.array(activityEvent))
142
141
  .check(z.describe('Every log in the transaction, decoded when possible, so nothing is hidden. Returned only when the request sets `logs=true`, except on `unknown` items where it is always present.')),
143
142
  id: z.string().check(z.describe('Stable activity ID built from the transaction hash and log index (`${transactionHash}-${logIndex}`).'), z.meta({
144
143
  examples: ['0x515801d7f9a5ac705e793e85904c9c69b3f1694b465cc8fb6ba3f0298dc82665-3'],
@@ -154,8 +153,7 @@ export var schema;
154
153
  .check(z.describe('Human-readable label for the activity `type`, suitable for display in a feed. Past-tense, sentence case (e.g. `Token transferred`, `Tokens swapped`, `Burn blocked`).'), z.meta({ examples: ['Token transferred'] })),
155
154
  transactionHash: Schema.Hash.check(z.describe('Transaction hash for this activity.')),
156
155
  };
157
- /** The classified activity item variants (discriminated by `type`). */
158
- schema.items = [
156
+ const itemSchemas = [
159
157
  z
160
158
  .object({
161
159
  ...base,
@@ -299,7 +297,7 @@ export var schema;
299
297
  z
300
298
  .object({
301
299
  ...base,
302
- data: schema.Transfer.check(z.describe('Transfer details with feed-relative direction and signer fields.')),
300
+ data: activityTransfer.check(z.describe('Transfer details with feed-relative direction and signer fields.')),
303
301
  type: z
304
302
  .literal('transfer')
305
303
  .check(z.describe('Activity type that tells you which `data` shape this entry uses.')),
@@ -1256,16 +1254,24 @@ export var schema;
1256
1254
  })
1257
1255
  .check(z.meta({ title: 'Unknown' })),
1258
1256
  ];
1257
+ /** The classified activity item variants (discriminated by `type`). */
1258
+ schema.items = itemSchemas.map((item) => {
1259
+ const [type] = item.shape.type.def.values;
1260
+ if (typeof type !== 'string')
1261
+ throw new Error('Activity item schemas require a string type.');
1262
+ const name = `ActivityItem${type
1263
+ .replace(/[^A-Za-z0-9]+(.)/g, (_, character) => character.toUpperCase())
1264
+ .replace(/^(.)/, (character) => character.toUpperCase())}`;
1265
+ return OpenApi.component(item, name);
1266
+ });
1259
1267
  /** A single classified activity item. */
1260
- schema.Item = z
1261
- .union(schema.items)
1262
- .check(z.describe('One classified activity item in the address feed.'));
1268
+ schema.Item = OpenApi.component(z.union(schema.items).check(z.describe('One classified activity item in the address feed.')), 'ActivityItem');
1263
1269
  /** A folded access-key group: consecutive same-signer items on one UTC day. */
1264
1270
  schema.Group = z
1265
1271
  .object({
1266
1272
  chainId: z
1267
1273
  .optional(Schema.ChainId)
1268
- .check(z.describe('Chain id containing this activity group. Returned for multi-Zone requests.'), z.meta({ examples: [421700001] })),
1274
+ .check(z.describe('Chain ID containing this activity group. Returned when `include=zones`.'), z.meta({ examples: [421700001] })),
1269
1275
  data: z
1270
1276
  .object({
1271
1277
  items: z.array(schema.Item).check(z.describe('Activity items included in this group.')),
@@ -1289,16 +1295,17 @@ export var schema;
1289
1295
  type: z.literal('group'),
1290
1296
  })
1291
1297
  .check(z.meta({ title: 'Group' }), z.describe('A group of consecutive activity items signed by the same access key.'));
1298
+ const activityGroup = OpenApi.component(schema.Group, 'ActivityGroup');
1292
1299
  /** A single activity item, or a folded access-key group. */
1293
- schema.Entry = z
1294
- .union([...schema.items, schema.Group])
1295
- .check(z.describe('Either one activity item, or a grouped set of access-key activity items.'));
1300
+ schema.Entry = OpenApi.component(z
1301
+ .union([schema.Item, activityGroup])
1302
+ .check(z.describe('Either one activity item, or a grouped set of access-key activity items.')), 'ActivityEntry');
1296
1303
  /** Optional activity resources and selection modes. */
1297
1304
  schema.Include = z
1298
1305
  .enum(includeFields)
1299
1306
  .check(z.describe('Additional activity resources and selection modes to include.'));
1300
1307
  /** Parses comma-separated optional activity resources and selection modes. */
1301
- schema.includeQuery = Schema.includeQuery(schema.Include, 'Comma-separated activity options, such as `zones,token.logoUri,token.verified`.');
1308
+ schema.includeQuery = Schema.includeQuery(schema.Include, 'Comma-separated activity options. `zones` combines the selected parent chain with every readable Zone.');
1302
1309
  /** Schemas for the getAddressActivities operation. */
1303
1310
  let getAddressActivities;
1304
1311
  (function (getAddressActivities) {
@@ -1333,7 +1340,7 @@ export var schema;
1333
1340
  * `group=true`) plus the root `nextCursor` pagination field shared with
1334
1341
  * other list endpoints.
1335
1342
  */
1336
- getAddressActivities.Response = z
1343
+ getAddressActivities.Response = OpenApi.component(z
1337
1344
  .object({
1338
1345
  data: z.array(schema.Entry).check(z.describe('Activity feed entries for the address.')),
1339
1346
  meta: z
@@ -1341,7 +1348,7 @@ export var schema;
1341
1348
  .check(z.describe('Page-level resources, such as valuation rate provenance.')),
1342
1349
  nextCursor: Schema.NextCursor,
1343
1350
  })
1344
- .check(z.describe('A page of human-readable activity for the address.'));
1351
+ .check(z.describe('A page of human-readable activity for the address.')), 'AddressActivityList');
1345
1352
  })(getAddressActivities = schema.getAddressActivities || (schema.getAddressActivities = {}));
1346
1353
  /** Schemas for the getTransactionActivities operation. */
1347
1354
  let getTransactionActivities;
@@ -1366,20 +1373,20 @@ export var schema;
1366
1373
  })
1367
1374
  .check(z.describe('Query parameters for the transaction activity request.'));
1368
1375
  /**
1369
- * The classified activity for one transaction: a list of `data` items
1370
- * (usually one headline item, occasionally more) describing what the
1371
- * transaction did onchain. Unpaginated a single transaction is finite
1372
- * so there is no `nextCursor`.
1376
+ * The classified activity for one transaction: a list of `data` items,
1377
+ * one per independent activity, describing what the transaction did
1378
+ * onchain. Unpaginated (a single transaction is finite), so there is no
1379
+ * `nextCursor`.
1373
1380
  */
1374
- getTransactionActivities.Response = z
1381
+ getTransactionActivities.Response = OpenApi.component(z
1375
1382
  .object({
1376
- chainId: Schema.ChainId.check(z.describe('Chain id containing the transaction.'), z.meta({ examples: [4217] })),
1383
+ chainId: Schema.ChainId.check(z.describe('Chain ID containing the transaction.'), z.meta({ examples: [4217] })),
1377
1384
  data: z.array(schema.Item).check(z.describe('Activity items classified from the transaction.')),
1378
1385
  meta: z
1379
1386
  .optional(schema.Meta)
1380
1387
  .check(z.describe('Page-level resources, such as valuation rate provenance.')),
1381
1388
  })
1382
- .check(z.describe('The human-readable activity that happened on a transaction.'));
1389
+ .check(z.describe('The human-readable activity that happened on a transaction.')), 'TransactionActivityList');
1383
1390
  })(getTransactionActivities = schema.getTransactionActivities || (schema.getTransactionActivities = {}));
1384
1391
  })(schema || (schema = {}));
1385
1392
  /**
@@ -1468,6 +1475,7 @@ const titleByEventName = {
1468
1475
  BlacklistUpdated: 'Blacklist updated',
1469
1476
  Burn: 'Token burned',
1470
1477
  BurnBlocked: 'Burn blocked',
1478
+ Deposit: 'Assets deposited',
1471
1479
  Deposited: 'Assets deposited',
1472
1480
  EarnDeposit: 'Private assets deposited',
1473
1481
  EarnRedeem: 'Private shares redeemed',
@@ -1476,6 +1484,7 @@ const titleByEventName = {
1476
1484
  RedeemRequested: 'Share redemption requested',
1477
1485
  Redeemed: 'Shares redeemed',
1478
1486
  VenueSharesDeposited: 'Shares deposited',
1487
+ Withdraw: 'Assets withdrawn',
1479
1488
  WithdrewExact: 'Assets withdrawn',
1480
1489
  ChannelClosed: 'Channel closed',
1481
1490
  ChannelOpened: 'Channel opened',
@@ -1539,12 +1548,43 @@ const closeRequestedSelector = '0xf5a36fc00a96cbb9cf1f8f59299165e1d8ffffe94396d8
1539
1548
  const accessKeySpendSelector = '0xe0815e3aaadddf4dd75bde97fc060f0c38afe18e87a169be86a3f5c28247f192';
1540
1549
  /** TIP-20 / ERC-20 `Transfer` event signature for the decoded CTE. */
1541
1550
  const transferSignature = 'event Transfer(address indexed from, address indexed to, uint256 value)';
1551
+ // Standard ERC-4626 vault events, absent from the merged `viem/tempo` ABI.
1552
+ // Decoding them lets external vault deposits/withdrawals classify instead of
1553
+ // falling through to the share-token mint/burn legs.
1554
+ const erc4626Events = [
1555
+ {
1556
+ inputs: [
1557
+ { indexed: true, name: 'sender', type: 'address' },
1558
+ { indexed: true, name: 'owner', type: 'address' },
1559
+ { indexed: false, name: 'assets', type: 'uint256' },
1560
+ { indexed: false, name: 'shares', type: 'uint256' },
1561
+ ],
1562
+ name: 'Deposit',
1563
+ type: 'event',
1564
+ },
1565
+ {
1566
+ inputs: [
1567
+ { indexed: true, name: 'sender', type: 'address' },
1568
+ { indexed: true, name: 'receiver', type: 'address' },
1569
+ { indexed: true, name: 'owner', type: 'address' },
1570
+ { indexed: false, name: 'assets', type: 'uint256' },
1571
+ { indexed: false, name: 'shares', type: 'uint256' },
1572
+ ],
1573
+ name: 'Withdraw',
1574
+ type: 'event',
1575
+ },
1576
+ ];
1542
1577
  // TIP-20 payment-channel reserve precompile. Not exported by `viem/tempo`
1543
1578
  // `Addresses`, so the known address is pinned here: channel events
1544
1579
  // (`ChannelOpened`/`Settled`/`TopUp`/`ChannelClosed`) are emitted here, and
1545
1580
  // their paired TIP-20 `Transfer` legs use it as the funding counterparty, which
1546
1581
  // is how `channel-funded`/`channel-settled`/`channel-closed` recover the token.
1547
1582
  const tip20ChannelReserve = '0x4d50500000000000000000000000000000000000';
1583
+ // Tempo system addresses other than the DEX are never swap-route
1584
+ // counterparties; a newly shipped precompile joins the exclusion automatically.
1585
+ const nonRouteSystemAddresses = new Set([...Object.values(Addresses), tip20ChannelReserve]
1586
+ .filter((value) => value !== Addresses.stablecoinDex)
1587
+ .map((value) => value.toLowerCase()));
1548
1588
  /** Number of grouped entries to target before stopping the page-fetch loop. */
1549
1589
  const minGroupEntries = 5;
1550
1590
  /** Largest number of items folded into a single access-key group. */
@@ -1596,7 +1636,12 @@ export function addresses(options = {}) {
1596
1636
  description: 'Get a human-readable feed of what an address has been doing onchain, including payments sent and received, swaps, approvals, and more.',
1597
1637
  operationId: 'getAddressActivities',
1598
1638
  responses: OpenApi.responses({
1599
- errors: { 502: 'Tempo could not read activity or token data from an upstream service.' },
1639
+ errors: {
1640
+ 400: {
1641
+ codes: ['address_invalid', 'chain_id_invalid', 'chain_id_unsupported', 'query_invalid'],
1642
+ },
1643
+ 502: 'Tempo could not read activity or token data from an upstream service.',
1644
+ },
1600
1645
  success: {
1601
1646
  description: 'A page of address activity entries.',
1602
1647
  schema: schema.getAddressActivities.Response,
@@ -1629,7 +1674,7 @@ export function addresses(options = {}) {
1629
1674
  const chainId = query.chainId ?? c.get('chainId');
1630
1675
  const listed = await Timing.time(c, 'address_activities', async () => {
1631
1676
  if (!query.include.includes('zones')) {
1632
- const result = await Store.memoize(() => list(tidx({ chainId, tidx: c.get('getTidx')(chainId) }), {
1677
+ const result = await Store.memoize((signal) => list(tidx({ chainId, signal, tidx: c.get('getTidx')(chainId) }), {
1633
1678
  address: address,
1634
1679
  cursor: query.cursor,
1635
1680
  group: query.group,
@@ -1646,29 +1691,31 @@ export function addresses(options = {}) {
1646
1691
  const zoneChainIds = c.get('zoneChainIds');
1647
1692
  if (!zoneChainIds)
1648
1693
  throw new Error('Inferred Zone selection was not resolved.');
1649
- const result = await Store.memoize(() => listZones({
1694
+ const chainIds = [chainId, ...zoneChainIds];
1695
+ const result = await Store.memoize((signal) => listChains({
1650
1696
  address: address,
1651
1697
  cursor: query.cursor,
1652
1698
  group: query.group,
1653
1699
  limit: query.limit,
1654
1700
  logs: query.logs,
1655
- providers: zoneChainIds.map((zoneChainId) => ({
1656
- chainId: zoneChainId,
1701
+ providers: chainIds.map((chainId) => ({
1702
+ chainId,
1657
1703
  provider: tidx({
1658
- chainId: zoneChainId,
1659
- tidx: c.get('getTidx')(zoneChainId),
1704
+ chainId,
1705
+ signal,
1706
+ tidx: c.get('getTidx')(chainId),
1660
1707
  }),
1661
1708
  })),
1662
1709
  store: c.get('store'),
1663
1710
  }), {
1664
- key: `activities:zones:v1:${zoneChainIds.join(',')}:${address.toLowerCase()}:${query.group ?? false}:${query.logs ?? false}:${query.cursor ?? 'head'}:${query.limit}`,
1711
+ key: `activities:zones:v2:${chainIds.join(',')}:${address.toLowerCase()}:${query.group ?? false}:${query.logs ?? false}:${query.cursor ?? 'head'}:${query.limit}`,
1665
1712
  store: c.get('store'),
1666
1713
  ttl: Ttl.seconds(15),
1667
1714
  });
1668
1715
  return { kind: 'zones', ...result };
1669
1716
  });
1670
1717
  const { items, rates } = listed.kind === 'zones'
1671
- ? await enrichZoneTokens(c, {
1718
+ ? await enrichChainTokens(c, {
1672
1719
  denomination: query['valuation.currency'],
1673
1720
  include: query.include,
1674
1721
  items: listed.items,
@@ -1704,7 +1751,7 @@ export function addresses(options = {}) {
1704
1751
  * Runs the same `groupByTx` → `classifyGroup` pipeline the address feed uses
1705
1752
  * over a single transaction's logs, anchored on the transaction sender as the
1706
1753
  * viewer (a transaction-scoped feed has no subject account). Returns the
1707
- * classified item(s) usually one headline activity with the full per-tx
1754
+ * classified items (one per independent activity) with the full per-tx
1708
1755
  * event set available via `logs=true`.
1709
1756
  */
1710
1757
  export function transactions(options = {}) {
@@ -1720,12 +1767,20 @@ export function transactions(options = {}) {
1720
1767
  operationId: 'getTransactionActivities',
1721
1768
  responses: OpenApi.responses({
1722
1769
  errors: {
1770
+ 400: {
1771
+ codes: [
1772
+ 'chain_id_invalid',
1773
+ 'chain_id_unsupported',
1774
+ 'query_invalid',
1775
+ 'transaction_invalid',
1776
+ ],
1777
+ },
1723
1778
  404: {
1724
1779
  description: 'No mined transaction was found for that hash.',
1725
1780
  codes: ['transaction_not_found'],
1726
1781
  },
1727
1782
  409: {
1728
- description: 'More than one readable Zone contains that transaction hash.',
1783
+ description: 'More than one readable chain contains that transaction hash.',
1729
1784
  codes: ['transaction_ambiguous'],
1730
1785
  },
1731
1786
  502: 'Tempo could not read activity or token data from an upstream service.',
@@ -1770,10 +1825,11 @@ export function transactions(options = {}) {
1770
1825
  const zoneChainIds = c.get('zoneChainIds');
1771
1826
  if (!zoneChainIds)
1772
1827
  throw new Error('Inferred Zone selection was not resolved.');
1828
+ const chainIds = [query.chainId ?? c.get('chainId'), ...zoneChainIds];
1773
1829
  // Cache only positive resolution. Missing transactions must remain
1774
- // visible as soon as their Zone indexer catches up.
1830
+ // visible as soon as any selected indexer catches up.
1775
1831
  const match = await Store.memoize(async () => {
1776
- const probes = await Promise.all(zoneChainIds.map(async (chainId) => {
1832
+ const probes = await Promise.all(chainIds.map(async (chainId) => {
1777
1833
  try {
1778
1834
  return {
1779
1835
  chainId,
@@ -1790,7 +1846,7 @@ export function transactions(options = {}) {
1790
1846
  }));
1791
1847
  return ZoneSelection.uniqueMatch(probes);
1792
1848
  }, {
1793
- key: `activities:zones:v1:${zoneChainIds.join(',')}:tx:${transactionHash.toLowerCase()}`,
1849
+ key: `activities:zones:v2:${chainIds.join(',')}:tx:${transactionHash.toLowerCase()}`,
1794
1850
  shouldCache: (match) => match.kind === 'selected',
1795
1851
  store: c.get('store'),
1796
1852
  ttl: Ttl.seconds(15),
@@ -1810,11 +1866,12 @@ export function transactions(options = {}) {
1810
1866
  return selected;
1811
1867
  const { chainId } = selected;
1812
1868
  const unenrichedItems = await Store.memoize(() => selected.receipt === null
1813
- ? getTransactionActivities({
1869
+ ? getTransactionActivities_inner({
1814
1870
  chainId,
1815
1871
  logs: query.logs,
1816
1872
  store: c.get('store'),
1817
1873
  tidx: c.get('getTidx')(chainId),
1874
+ time: (name, fn) => Timing.time(c, name, fn),
1818
1875
  transactionHash,
1819
1876
  })
1820
1877
  : classifyTransactionActivities({
@@ -1823,6 +1880,7 @@ export function transactions(options = {}) {
1823
1880
  receipt: selected.receipt,
1824
1881
  store: c.get('store'),
1825
1882
  tidx: c.get('getTidx')(chainId),
1883
+ time: (name, fn) => Timing.time(c, name, fn),
1826
1884
  transactionHash,
1827
1885
  }), {
1828
1886
  key: `activities:v1:${chainId}:tx:${transactionHash.toLowerCase()}:${query.logs ?? false}`,
@@ -1842,7 +1900,7 @@ export function transactions(options = {}) {
1842
1900
  if (result.kind === 'ambiguous')
1843
1901
  return Response.error(c, {
1844
1902
  code: 'transaction_ambiguous',
1845
- message: 'Transaction hash matched more than one readable Zone',
1903
+ message: 'Transaction hash matched more than one readable chain',
1846
1904
  status: 409,
1847
1905
  });
1848
1906
  // Mined activity can carry current valuations and verification data.
@@ -1881,7 +1939,13 @@ export function transactions(options = {}) {
1881
1939
  * Items carry stub tokens; the caller enriches them via {@link enrichTokens}.
1882
1940
  */
1883
1941
  export async function getTransactionActivities(options) {
1884
- const receipt = await getTransactionReceipt(options);
1942
+ return getTransactionActivities_inner({
1943
+ ...options,
1944
+ time: async (_name, fn) => fn(),
1945
+ });
1946
+ }
1947
+ async function getTransactionActivities_inner(options) {
1948
+ const receipt = await options.time('transaction_activity_receipt', () => getTransactionReceipt(options));
1885
1949
  if (!receipt)
1886
1950
  return null;
1887
1951
  return classifyTransactionActivities({ ...options, receipt });
@@ -1910,13 +1974,15 @@ async function classifyTransactionActivities(options) {
1910
1974
  const { chainId, logs = false, receipt, tidx: tidxClient } = options;
1911
1975
  const hash = options.transactionHash.toLowerCase();
1912
1976
  const { blockNum, feePayer, txSender } = receipt;
1913
- const fingerprintMap_promise = Mpp.fingerprintMap({ store: options.store }).catch(() => ({}));
1977
+ const fingerprintMap_promise = options
1978
+ .time('transaction_activity_mpp', () => Mpp.fingerprintMap({ store: options.store }))
1979
+ .catch(() => ({}));
1914
1980
  // `logs` is sorted by block position, so a bare `tx_hash =` scans to genesis;
1915
1981
  // pair it with the receipt's block number (see `embedReceipts` in receipts.ts).
1916
- const logsResult = await tidxClient.fetch({
1982
+ const logsResult = await options.time('transaction_activity_logs', () => tidxClient.fetch({
1917
1983
  chainId,
1918
1984
  query: `SELECT tx_hash, block_num, block_timestamp, tx_idx, log_idx, address, selector, topic1, topic2, topic3, data FROM logs WHERE block_num = ${blockNum} AND tx_hash = '${hash}' ORDER BY log_idx ASC`,
1919
- });
1985
+ }));
1920
1986
  const rows = logsResult.rows.map((r) => ({
1921
1987
  blockNum: Number(Value.toNumber(r['block_num'])),
1922
1988
  blockTimestamp: Number(Value.toNumber(r['block_timestamp'])),
@@ -1993,17 +2059,17 @@ export async function list(provider, options) {
1993
2059
  // public token references and structured amounts via {@link enrichTokens}.
1994
2060
  return { items: allItems, nextCursor };
1995
2061
  }
1996
- const zoneCursorVersion = 1;
2062
+ const chainCursorVersion = 2;
1997
2063
  /**
1998
- * Lists address activity across multiple Zones in one stable timestamp-ordered
1999
- * feed. Its opaque cursor retains each Zone provider's native continuation.
2064
+ * Lists address activity across multiple chains in one stable timestamp-ordered
2065
+ * feed. Its opaque cursor retains each provider's native continuation.
2000
2066
  */
2001
- export async function listZones(options) {
2067
+ export async function listChains(options) {
2002
2068
  const { address, cursor, limit = 20, logs = false, providers, store } = options;
2003
2069
  const group = options.group ?? false;
2004
2070
  const orderedProviders = [...providers].sort((a, b) => a.chainId - b.chainId);
2005
2071
  const chainIds = orderedProviders.map(({ chainId }) => chainId);
2006
- const cursors = decodeZoneCursor(cursor, chainIds);
2072
+ const cursors = decodeChainCursor(cursor, chainIds);
2007
2073
  const viewer = address.toLowerCase();
2008
2074
  const fingerprintMap_promise = Mpp.fingerprintMap({ store }).catch(() => ({}));
2009
2075
  const states = orderedProviders.map(({ chainId, provider }) => ({
@@ -2076,16 +2142,16 @@ export async function listZones(options) {
2076
2142
  hasMore = states.some((state) => !state.exhausted || state.groups.length > 0);
2077
2143
  return {
2078
2144
  items: allItems,
2079
- nextCursor: hasMore ? encodeZoneCursor(cursors, chainIds) : null,
2145
+ nextCursor: hasMore ? encodeChainCursor(cursors, chainIds) : null,
2080
2146
  };
2081
2147
  }
2082
- function decodeZoneCursor(cursor, chainIds) {
2148
+ function decodeChainCursor(cursor, chainIds) {
2083
2149
  const fields = ['int'];
2084
2150
  for (const _chainId of chainIds)
2085
2151
  fields.push('int', 'id');
2086
2152
  const decoded = cursor ? Cursor.decode(cursor, fields) : undefined;
2087
2153
  const cursors = new Map();
2088
- if (!decoded || decoded[0] !== zoneCursorVersion)
2154
+ if (!decoded || decoded[0] !== chainCursorVersion)
2089
2155
  return cursors;
2090
2156
  for (let index = 0; index < chainIds.length; index++) {
2091
2157
  const chainId = chainIds[index];
@@ -2098,9 +2164,9 @@ function decodeZoneCursor(cursor, chainIds) {
2098
2164
  }
2099
2165
  return cursors;
2100
2166
  }
2101
- function encodeZoneCursor(cursors, chainIds) {
2167
+ function encodeChainCursor(cursors, chainIds) {
2102
2168
  return Cursor.encode([
2103
- zoneCursorVersion,
2169
+ chainCursorVersion,
2104
2170
  ...chainIds.flatMap((chainId) => [chainId, cursors.get(chainId) ?? 'head']),
2105
2171
  ]);
2106
2172
  }
@@ -2114,7 +2180,16 @@ function encodeZoneCursor(cursors, chainIds) {
2114
2180
  * (catches access-key-signed sends where `tx.from` differs from the address).
2115
2181
  */
2116
2182
  export function tidx(deps) {
2117
- const { chainId, tidx: tidxClient } = deps;
2183
+ const { chainId, signal, tidx: tidxClient } = deps;
2184
+ const fetch = (options) => {
2185
+ const requestSignal = signal && options.signal
2186
+ ? AbortSignal.any([signal, options.signal])
2187
+ : (signal ?? options.signal);
2188
+ return tidxClient.fetch({
2189
+ ...options,
2190
+ ...(requestSignal ? { signal: requestSignal } : {}),
2191
+ });
2192
+ };
2118
2193
  function parseCursor(cursor) {
2119
2194
  // Opaque `[blockNum, txIdx]` keyset cursor, the same encoding every other
2120
2195
  // list endpoint uses; a malformed/forged cursor decodes to `undefined` and
@@ -2134,7 +2209,7 @@ export function tidx(deps) {
2134
2209
  ? `AND block_num <= ${cursor.blockNum} AND (block_num < ${cursor.blockNum} OR tx_idx < ${cursor.txIdx})`
2135
2210
  : '';
2136
2211
  const queryLimit = limit + 1;
2137
- const fetchReceipts = (side) => tidxClient.fetch({
2212
+ const fetchReceipts = (side) => fetch({
2138
2213
  chainId,
2139
2214
  // Raw-table query (no signature-decoded CTE), so the inline SQL is cast
2140
2215
  // to `string`.
@@ -2178,7 +2253,7 @@ export function tidx(deps) {
2178
2253
  const txHashes = receipts.map((receipt) => `'${receipt.txHash}'`).join(', ');
2179
2254
  // `logs` is not keyed by transaction hash, so pair the bounded hash list
2180
2255
  // with the selected receipts' block range to avoid a full archive scan.
2181
- const logs = await tidxClient.fetch({
2256
+ const logs = await fetch({
2182
2257
  chainId,
2183
2258
  // Raw-table query (no signature-decoded CTE), so the inline SQL is cast
2184
2259
  // to `string`.
@@ -2231,7 +2306,7 @@ export function tidx(deps) {
2231
2306
  `;
2232
2307
  const result = await (async () => {
2233
2308
  try {
2234
- return await tidxClient.fetch({
2309
+ return await fetch({
2235
2310
  chainId,
2236
2311
  query: query('block_num'),
2237
2312
  signatures: [transferSignature],
@@ -2242,7 +2317,7 @@ export function tidx(deps) {
2242
2317
  throw error;
2243
2318
  try {
2244
2319
  // The expression blocks a mis-selected block-ordered top-N walk.
2245
- return await tidxClient.fetch({
2320
+ return await fetch({
2246
2321
  chainId,
2247
2322
  query: query('block_num + 0'),
2248
2323
  signatures: [transferSignature],
@@ -2251,7 +2326,7 @@ export function tidx(deps) {
2251
2326
  catch (fallbackError) {
2252
2327
  if (!Tidx.isDeterministicError(fallbackError))
2253
2328
  throw fallbackError;
2254
- const result = await tidxClient.fetch({
2329
+ const result = await fetch({
2255
2330
  chainId,
2256
2331
  query: query(),
2257
2332
  signatures: [transferSignature],
@@ -2309,8 +2384,10 @@ export function mergeAndSort(q1, q2, q3) {
2309
2384
  const q1Hashes = new Set(q1.map((r) => r.txHash));
2310
2385
  const filteredQ2 = q2.filter((r) => !q1Hashes.has(r.txHash));
2311
2386
  const filteredQ3 = q3.filter((r) => !q1Hashes.has(r.txHash));
2312
- const q2Hashes = new Set(filteredQ2.map((r) => r.txHash));
2313
- const dedupedQ3 = filteredQ3.filter((r) => !q2Hashes.has(r.txHash));
2387
+ // Dedup by row, not transaction: a tx that both credits and debits the
2388
+ // viewer keeps its rows from both sides, one item each.
2389
+ const q2Keys = new Set(filteredQ2.map((r) => `${r.txHash}:${r.logIdx}`));
2390
+ const dedupedQ3 = filteredQ3.filter((r) => !q2Keys.has(`${r.txHash}:${r.logIdx}`));
2314
2391
  return [...q1, ...filteredQ2, ...dedupedQ3].sort((a, b) => {
2315
2392
  if (a.blockNum !== b.blockNum)
2316
2393
  return b.blockNum - a.blockNum;
@@ -2375,34 +2452,38 @@ function classifyGroup(group, viewer, fingerprintMap, includeLogs = false) {
2375
2452
  signer,
2376
2453
  viewer,
2377
2454
  });
2378
- // Q2-only — a transfer row without a joined receipt. Classify by comparing
2379
- // `from`/`to` against the viewer (zero-address sides are mints/burns).
2380
- if (q2Rows.length > 0) {
2381
- const r = q2Rows[0];
2382
- const base = itemBase(r.logIdx);
2383
- const from = r.transferFrom;
2384
- const to = r.transferTo;
2385
- const sourceToken = token(r.transferAmount, r.transferToken);
2386
- if (Address.isEqual(from, zeroAddress))
2387
- return [{ ...base, data: { recipient: viewer, sourceToken }, type: 'mint' }];
2388
- if (Address.isEqual(to, zeroAddress))
2389
- return [{ ...base, data: { sender: from, signer, sourceToken }, type: 'burn' }];
2390
- // Q2-only rows carry no memo, so MPP service attribution is unavailable
2391
- // here; `attribution` is resolved only on the log-bearing (Q1) path.
2392
- if (Address.isEqual(from, viewer))
2393
- return [
2394
- transfer(base, blockNum, {
2455
+ // Q2-only: transfer rows without a joined receipt, one item per row so a
2456
+ // batch paying the viewer several times surfaces every payment. Classify by
2457
+ // comparing `from`/`to` against the viewer (zero-address sides mint/burn).
2458
+ if (q2Rows.length > 0)
2459
+ return [...q2Rows]
2460
+ .sort((a, b) => a.logIdx - b.logIdx)
2461
+ .map((r) => {
2462
+ const base = itemBase(r.logIdx);
2463
+ const from = r.transferFrom;
2464
+ const to = r.transferTo;
2465
+ const sourceToken = token(r.transferAmount, r.transferToken);
2466
+ if (Address.isEqual(from, zeroAddress))
2467
+ return { ...base, data: { recipient: viewer, sourceToken }, type: 'mint' };
2468
+ if (Address.isEqual(to, zeroAddress))
2469
+ return { ...base, data: { sender: from, signer, sourceToken }, type: 'burn' };
2470
+ // Q2-only rows carry no memo, so MPP service attribution is unavailable
2471
+ // here; `attribution` is resolved only on the log-bearing (Q1) path.
2472
+ if (Address.isEqual(from, viewer))
2473
+ return transfer(base, blockNum, {
2395
2474
  direction: 'out',
2396
2475
  recipient: to,
2397
2476
  sender: viewer,
2398
2477
  signer,
2399
2478
  sourceToken,
2400
- }),
2401
- ];
2402
- return [
2403
- transfer(base, blockNum, { direction: 'in', recipient: viewer, sender: from, sourceToken }),
2404
- ];
2405
- }
2479
+ });
2480
+ return transfer(base, blockNum, {
2481
+ direction: 'in',
2482
+ recipient: viewer,
2483
+ sender: from,
2484
+ sourceToken,
2485
+ });
2486
+ });
2406
2487
  // Fallback — should not happen (a group with neither q1 nor q2 rows).
2407
2488
  return [
2408
2489
  unknown(itemBase(Math.min(...group.rows.map((r) => r.logIdx))), {
@@ -2430,7 +2511,7 @@ function classifyFromLogs(rows, ctx) {
2430
2511
  })();
2431
2512
  // When the caller requested raw logs, attach the full per-transaction log set
2432
2513
  // (decoded when possible) to every classified item so no co-located log is
2433
- // hidden behind the headline. `unknown` items already carry it.
2514
+ // hidden. `unknown` items already carry it.
2434
2515
  if (!ctx.includeLogs)
2435
2516
  return items;
2436
2517
  const events = buildTxEvents(rows, decoded);
@@ -2447,1214 +2528,1453 @@ function signerForRows(rows, options) {
2447
2528
  ? accessKeySigner
2448
2529
  : options.fallback;
2449
2530
  }
2450
- /** Classifies a Q1 (log-bearing) group via `parseEventLogs`, picking one headline item. */
2531
+ /**
2532
+ * Classifies every activity in a Q1 (log-bearing) transaction. Recognizers run
2533
+ * in priority order over a pool of decoded events, each emitting one item per
2534
+ * matched anchor and claiming the exact logs backing it.
2535
+ */
2451
2536
  function classifyFromLogsCore(rows, ctx, events = decodeRows(rows)) {
2452
2537
  const { blockNum, fingerprintMap, itemBase, viewer } = ctx;
2453
2538
  const signer = signerForRows(rows, { fallback: ctx.signer, viewer });
2454
2539
  const minLogIndex = Math.min(...rows.map((r) => r.logIdx));
2455
- // Payment-channel settlement: a CloseRequested topic plus an optional refund
2456
- // transfer back to the viewer.
2457
- const hasCloseRequested = rows.some((r) => r.selector === closeRequestedSelector);
2458
- if (hasCloseRequested) {
2459
- const closeRequestedLogIndex = rows.find((r) => r.selector === closeRequestedSelector)?.logIdx ?? minLogIndex;
2460
- const refundTransfer = events.find((e) => (e.eventName === 'Transfer' || e.eventName === 'TransferWithMemo') &&
2461
- 'to' in e.args &&
2462
- Address.isEqual(e.args.to, viewer));
2463
- const refund = refundTransfer
2464
- ? token(refundTransfer.args.amount, refundTransfer.address)
2465
- : undefined;
2466
- return [
2467
- {
2468
- ...itemBase(closeRequestedLogIndex),
2469
- data: { ...(refund ? { refund } : {}), signer },
2470
- type: 'session-closed',
2471
- },
2472
- ];
2473
- }
2474
- // Dedup preference: TransferWithMemo beats Transfer; Mint/Burn beat
2475
- // zero-address Transfer.
2476
- const preferenceMap = new Map();
2540
+ // Pair folds are counted: each TransferWithMemo folds one matching plain
2541
+ // Transfer and each Mint/Burn event folds one zero-address transfer, so an
2542
+ // extra identical payment survives as its own item. Keys require the full
2543
+ // event shape; a malformed event must not fold away a classifiable leg.
2544
+ const foldCounts = new Map();
2545
+ const addFold = (key) => foldCounts.set(key, (foldCounts.get(key) ?? 0) + 1);
2477
2546
  for (const event of events) {
2478
2547
  if (event.eventName === 'TransferWithMemo' && 'from' in event.args && 'to' in event.args) {
2479
2548
  const args = event.args;
2480
- preferenceMap.set(`${args.from}:${args.to}:${args.amount}`, 'TransferWithMemo');
2549
+ addFold(`${event.address}:${args.from}:${args.to}:${args.amount}`);
2481
2550
  }
2482
- if (event.eventName === 'Mint' && 'amount' in event.args)
2483
- preferenceMap.set(`mint:${event.address}:${event.args.amount}`, 'Mint');
2484
- if (event.eventName === 'Burn' && 'amount' in event.args)
2485
- preferenceMap.set(`burn:${event.address}:${event.args.amount}`, 'Burn');
2551
+ if (event.eventName === 'Mint' && 'amount' in event.args && 'to' in event.args)
2552
+ addFold(`mint:${event.address}:${event.args.amount}`);
2553
+ if (event.eventName === 'Burn' && 'amount' in event.args && 'from' in event.args)
2554
+ addFold(`burn:${event.address}:${event.args.amount}`);
2486
2555
  }
2487
- const dedupedEvents = events.filter((event) => {
2488
- if (event.eventName !== 'Transfer')
2556
+ const takeFold = (key) => {
2557
+ const count = foldCounts.get(key) ?? 0;
2558
+ if (count === 0)
2559
+ return false;
2560
+ foldCounts.set(key, count - 1);
2561
+ return true;
2562
+ };
2563
+ const finalEvents = events.filter((event) => {
2564
+ if (event.eventName !== 'Transfer' && event.eventName !== 'TransferWithMemo')
2565
+ return true;
2566
+ if (!('from' in event.args) || !('to' in event.args))
2489
2567
  return true;
2490
2568
  const args = event.args;
2491
- if (preferenceMap.has(`${args.from}:${args.to}:${args.amount}`))
2569
+ if (event.eventName === 'Transfer' &&
2570
+ takeFold(`${event.address}:${args.from}:${args.to}:${args.amount}`))
2492
2571
  return false;
2493
2572
  if (Address.isEqual(args.from, zeroAddress) &&
2494
- preferenceMap.has(`mint:${event.address}:${args.amount}`))
2573
+ takeFold(`mint:${event.address}:${args.amount}`))
2495
2574
  return false;
2496
2575
  if (Address.isEqual(args.to, zeroAddress) &&
2497
- preferenceMap.has(`burn:${event.address}:${args.amount}`))
2576
+ takeFold(`burn:${event.address}:${args.amount}`))
2498
2577
  return false;
2499
2578
  return true;
2500
2579
  });
2501
- const finalEvents = dedupedEvents.filter((event) => {
2502
- if (event.eventName !== 'TransferWithMemo')
2580
+ const pool = [...finalEvents].sort((a, b) => a.logIndex - b.logIndex);
2581
+ const claimed = new Set();
2582
+ const items = [];
2583
+ // Claims made while building an item are journaled, so a build that throws
2584
+ // on a malformed event releases its legs and consumes only the anchor.
2585
+ let journal;
2586
+ const claim = (event) => {
2587
+ claimed.add(event.logIndex);
2588
+ journal?.push(event.logIndex);
2589
+ };
2590
+ // A predicate throwing on a malformed decode must not discard the whole
2591
+ // transaction; treat that event as a non-match instead.
2592
+ const unclaimed = (predicate) => pool.filter((event) => {
2593
+ if (claimed.has(event.logIndex))
2594
+ return false;
2595
+ if (!predicate)
2503
2596
  return true;
2504
- const args = event.args;
2505
- if (Address.isEqual(args.from, zeroAddress) &&
2506
- preferenceMap.has(`mint:${event.address}:${args.amount}`))
2597
+ try {
2598
+ return predicate(event);
2599
+ }
2600
+ catch {
2507
2601
  return false;
2508
- if (Address.isEqual(args.to, zeroAddress) &&
2509
- preferenceMap.has(`burn:${event.address}:${args.amount}`))
2602
+ }
2603
+ });
2604
+ const lc = (value) => value.toLowerCase();
2605
+ const hx = (value) => value.toLowerCase();
2606
+ // Non-strict decoding can surface Transfer-shaped events with malformed
2607
+ // args; validating (once per event) keeps predicate comparisons throw-free.
2608
+ const transferIndexes = new Set(pool
2609
+ .filter((event) => {
2610
+ if (event.eventName !== 'Transfer' && event.eventName !== 'TransferWithMemo')
2510
2611
  return false;
2511
- return true;
2612
+ const args = event.args;
2613
+ return (typeof args.amount === 'bigint' &&
2614
+ typeof args.from === 'string' &&
2615
+ Address.validate(args.from) &&
2616
+ typeof args.to === 'string' &&
2617
+ Address.validate(args.to));
2618
+ })
2619
+ .map((event) => event.logIndex));
2620
+ const isTransfer = (event) => transferIndexes.has(event.logIndex);
2621
+ const transferArgs = (event) => event.args;
2622
+ // Finds and claims the first unclaimed event matching `predicate`, so a
2623
+ // later identical anchor pairs with the next matching leg instead of
2624
+ // reusing this one.
2625
+ const takeEvent = (predicate) => {
2626
+ for (const event of pool) {
2627
+ if (claimed.has(event.logIndex))
2628
+ continue;
2629
+ const match = (() => {
2630
+ try {
2631
+ return predicate(event);
2632
+ }
2633
+ catch {
2634
+ return false;
2635
+ }
2636
+ })();
2637
+ if (!match)
2638
+ continue;
2639
+ claim(event);
2640
+ return event;
2641
+ }
2642
+ return undefined;
2643
+ };
2644
+ const takeTransfer = (predicate) => takeEvent((event) => isTransfer(event) && predicate(event));
2645
+ // Runs `build` once per unclaimed matching event, in log order; `build`
2646
+ // claims its legs and returns the item(s) to emit, or nothing for
2647
+ // fold-only recognitions. A throwing build consumes only its anchor.
2648
+ const recognize = (predicate, build) => {
2649
+ for (const anchor of unclaimed(predicate)) {
2650
+ // A build may claim a later anchor of the same pass (or a log decoded
2651
+ // into duplicate ABI copies); never process a claimed anchor again.
2652
+ if (claimed.has(anchor.logIndex))
2653
+ continue;
2654
+ claim(anchor);
2655
+ journal = [];
2656
+ try {
2657
+ const built = build(anchor);
2658
+ if (built)
2659
+ items.push(...(Array.isArray(built) ? built : [built]));
2660
+ }
2661
+ catch {
2662
+ // Indexed logs can match a known signature without satisfying that
2663
+ // event's expected shape; release the legs and skip the anchor.
2664
+ for (const index of journal ?? [])
2665
+ claimed.delete(index);
2666
+ }
2667
+ finally {
2668
+ journal = undefined;
2669
+ }
2670
+ }
2671
+ };
2672
+ const named = (name) => (event) => event.eventName === name;
2673
+ // Claims and returns a vault operation's token legs, matched by role and
2674
+ // amount: asset legs in or out of the vault, share mint/burn legs on it.
2675
+ const claimVaultLegs = (vault, amounts) => {
2676
+ const legs = [];
2677
+ for (const event of unclaimed()) {
2678
+ const isVaultMintBurn = (event.eventName === 'Mint' || event.eventName === 'Burn') &&
2679
+ Address.isEqual(event.address, vault);
2680
+ const isVaultLeg = (() => {
2681
+ if (!isTransfer(event))
2682
+ return false;
2683
+ const args = transferArgs(event);
2684
+ if (Address.isEqual(args.from, vault) || Address.isEqual(args.to, vault))
2685
+ return true;
2686
+ return (Address.isEqual(event.address, vault) &&
2687
+ (Address.isEqual(args.from, zeroAddress) ||
2688
+ Address.isEqual(args.to, zeroAddress)));
2689
+ })();
2690
+ if (!isVaultMintBurn && !isVaultLeg)
2691
+ continue;
2692
+ const amount = event.args.amount;
2693
+ if (amount === undefined || !amounts.includes(amount))
2694
+ continue;
2695
+ claim(event);
2696
+ legs.push(event);
2697
+ }
2698
+ // The allowance granted for this operation (approve+deposit multicall) is
2699
+ // internal: exact amount, spender is the vault, on a token a leg moved.
2700
+ const legTokens = new Set(legs.map((leg) => leg.address.toLowerCase()));
2701
+ for (const event of unclaimed()) {
2702
+ if (event.eventName !== 'Approval' || !('spender' in event.args))
2703
+ continue;
2704
+ if (!legTokens.has(event.address.toLowerCase()))
2705
+ continue;
2706
+ if (!Address.isEqual(event.args.spender, vault))
2707
+ continue;
2708
+ const amount = event.args.amount;
2709
+ if (amount === undefined || !amounts.includes(amount))
2710
+ continue;
2711
+ claim(event);
2712
+ legs.push(event);
2713
+ }
2714
+ return legs;
2715
+ };
2716
+ // Private Earn router operations. Each wraps a public vault operation on the
2717
+ // vault it names; fold that vault's own event and token legs, leaving vault
2718
+ // activity elsewhere in the batch unclaimed.
2719
+ recognize(named('EarnDeposit'), (event) => {
2720
+ const args = event.args;
2721
+ const vault = lc(args.earnVault);
2722
+ // Parse before claiming so a malformed event consumes nothing but itself.
2723
+ const data = {
2724
+ actionId: args.actionId,
2725
+ assets: args.vaultAssets.toString(),
2726
+ inputAmount: args.inputAmount.toString(),
2727
+ inputToken: lc(args.inputToken),
2728
+ shares: args.earnShares.toString(),
2729
+ signer,
2730
+ status: 'completed',
2731
+ vault,
2732
+ zoneDepositHash: args.zoneDepositHash,
2733
+ };
2734
+ // Fold the vault event this router call produced: the nearest preceding
2735
+ // amount match, so a separate same-vault deposit keeps its own.
2736
+ const nested = unclaimed((e) => e.eventName === 'Deposited' &&
2737
+ e.logIndex < event.logIndex &&
2738
+ Address.isEqual(e.address, vault) &&
2739
+ e.args.assets === args.vaultAssets).at(-1);
2740
+ if (nested)
2741
+ claim(nested);
2742
+ else
2743
+ takeEvent((e) => e.eventName === 'Deposited' && Address.isEqual(e.address, vault));
2744
+ claimVaultLegs(vault, [args.inputAmount, args.vaultAssets, args.earnShares]);
2745
+ // The funding leg pays the router (the emitting contract), not the vault.
2746
+ claimVaultLegs(event.address, [args.inputAmount]);
2747
+ return { ...itemBase(event.logIndex), data, type: 'private-assets-deposited' };
2512
2748
  });
2513
- const transfers = finalEvents.filter((e) => (e.eventName === 'Transfer' || e.eventName === 'TransferWithMemo') &&
2514
- 'to' in e.args &&
2515
- 'from' in e.args &&
2516
- 'amount' in e.args);
2517
- const privateDeposit = finalEvents.find((event) => event.eventName === 'EarnDeposit');
2518
- if (privateDeposit) {
2519
- const args = privateDeposit.args;
2520
- return [
2521
- {
2522
- ...itemBase(logIndexOfEvent(privateDeposit)),
2523
- data: {
2524
- actionId: args.actionId,
2525
- assets: args.vaultAssets.toString(),
2526
- inputAmount: args.inputAmount.toString(),
2527
- inputToken: args.inputToken.toLowerCase(),
2528
- shares: args.earnShares.toString(),
2529
- signer,
2530
- status: 'completed',
2531
- vault: args.earnVault.toLowerCase(),
2532
- zoneDepositHash: args.zoneDepositHash,
2533
- },
2534
- type: 'private-assets-deposited',
2535
- },
2536
- ];
2537
- }
2538
- const privateRedeem = finalEvents.find((event) => event.eventName === 'EarnRedeem');
2539
- if (privateRedeem) {
2540
- const args = privateRedeem.args;
2541
- return [
2542
- {
2543
- ...itemBase(logIndexOfEvent(privateRedeem)),
2544
- data: {
2545
- actionId: args.actionId,
2546
- assets: args.vaultAssets.toString(),
2547
- outputAmount: args.outputAmount.toString(),
2548
- outputToken: args.outputToken.toLowerCase(),
2549
- shares: args.earnShares.toString(),
2550
- signer,
2551
- status: 'completed',
2552
- vault: args.earnVault.toLowerCase(),
2553
- zoneDepositHash: args.zoneDepositHash,
2554
- },
2555
- type: 'private-shares-redeemed',
2749
+ recognize(named('EarnRedeem'), (event) => {
2750
+ const args = event.args;
2751
+ const vault = lc(args.earnVault);
2752
+ // Parse before claiming so a malformed event consumes nothing but itself.
2753
+ const data = {
2754
+ actionId: args.actionId,
2755
+ assets: args.vaultAssets.toString(),
2756
+ outputAmount: args.outputAmount.toString(),
2757
+ outputToken: lc(args.outputToken),
2758
+ shares: args.earnShares.toString(),
2759
+ signer,
2760
+ status: 'completed',
2761
+ vault,
2762
+ zoneDepositHash: args.zoneDepositHash,
2763
+ };
2764
+ // Fold the vault event this router call produced: the nearest preceding
2765
+ // amount match, so a separate same-vault redemption keeps its own.
2766
+ const nested = unclaimed((e) => (e.eventName === 'Redeemed' || e.eventName === 'WithdrewExact') &&
2767
+ e.logIndex < event.logIndex &&
2768
+ Address.isEqual(e.address, vault) &&
2769
+ e.args.assets === args.vaultAssets).at(-1);
2770
+ if (nested)
2771
+ claim(nested);
2772
+ else
2773
+ takeEvent((e) => (e.eventName === 'Redeemed' || e.eventName === 'WithdrewExact') &&
2774
+ Address.isEqual(e.address, vault));
2775
+ claimVaultLegs(vault, [args.outputAmount, args.vaultAssets, args.earnShares]);
2776
+ // The payout leg leaves through the router (the emitting contract).
2777
+ claimVaultLegs(event.address, [args.outputAmount]);
2778
+ return { ...itemBase(event.logIndex), data, type: 'private-shares-redeemed' };
2779
+ });
2780
+ // Earn vault operations, each claiming its own token legs.
2781
+ recognize(named('Deposited'), (event) => {
2782
+ const args = event.args;
2783
+ claimVaultLegs(event.address, [args.assets, args.earnShares]);
2784
+ return {
2785
+ ...itemBase(event.logIndex),
2786
+ data: {
2787
+ assets: args.assets.toString(),
2788
+ caller: lc(args.caller),
2789
+ receiver: lc(args.receiver),
2790
+ shares: args.earnShares.toString(),
2791
+ signer,
2792
+ status: 'completed',
2793
+ vault: lc(event.address),
2556
2794
  },
2557
- ];
2558
- }
2559
- const deposited = finalEvents.find((event) => event.eventName === 'Deposited');
2560
- if (deposited) {
2561
- const args = deposited.args;
2562
- return [
2563
- {
2564
- ...itemBase(logIndexOfEvent(deposited)),
2565
- data: {
2566
- assets: args.assets.toString(),
2567
- caller: args.caller.toLowerCase(),
2568
- receiver: args.receiver.toLowerCase(),
2569
- shares: args.earnShares.toString(),
2570
- signer,
2571
- status: 'completed',
2572
- vault: deposited.address.toLowerCase(),
2573
- },
2574
- type: 'assets-deposited',
2795
+ type: 'assets-deposited',
2796
+ };
2797
+ });
2798
+ recognize(named('VenueSharesDeposited'), (event) => {
2799
+ const args = event.args;
2800
+ claimVaultLegs(event.address, [
2801
+ args.earnShares,
2802
+ args.receivedEngineShares,
2803
+ args.requestedVenueShares,
2804
+ ]);
2805
+ return {
2806
+ ...itemBase(event.logIndex),
2807
+ data: {
2808
+ caller: lc(args.caller),
2809
+ receivedEngineShares: args.receivedEngineShares.toString(),
2810
+ receiver: lc(args.receiver),
2811
+ requestedVenueShares: args.requestedVenueShares.toString(),
2812
+ shares: args.earnShares.toString(),
2813
+ signer,
2814
+ status: 'completed',
2815
+ vault: lc(event.address),
2575
2816
  },
2576
- ];
2577
- }
2578
- const venueDeposit = finalEvents.find((event) => event.eventName === 'VenueSharesDeposited');
2579
- if (venueDeposit) {
2580
- const args = venueDeposit.args;
2581
- return [
2582
- {
2583
- ...itemBase(logIndexOfEvent(venueDeposit)),
2584
- data: {
2585
- caller: args.caller.toLowerCase(),
2586
- receivedEngineShares: args.receivedEngineShares.toString(),
2587
- receiver: args.receiver.toLowerCase(),
2588
- requestedVenueShares: args.requestedVenueShares.toString(),
2589
- shares: args.earnShares.toString(),
2590
- signer,
2591
- status: 'completed',
2592
- vault: venueDeposit.address.toLowerCase(),
2593
- },
2594
- type: 'shares-deposited',
2817
+ type: 'shares-deposited',
2818
+ };
2819
+ });
2820
+ recognize(named('Redeemed'), (event) => {
2821
+ const args = event.args;
2822
+ claimVaultLegs(event.address, [args.assets, args.earnShares]);
2823
+ return {
2824
+ ...itemBase(event.logIndex),
2825
+ data: {
2826
+ assets: args.assets.toString(),
2827
+ caller: lc(args.caller),
2828
+ receiver: lc(args.receiver),
2829
+ shares: args.earnShares.toString(),
2830
+ signer,
2831
+ status: 'completed',
2832
+ vault: lc(event.address),
2595
2833
  },
2596
- ];
2597
- }
2598
- const redeemed = finalEvents.find((event) => event.eventName === 'Redeemed');
2599
- if (redeemed) {
2600
- const args = redeemed.args;
2601
- return [
2602
- {
2603
- ...itemBase(logIndexOfEvent(redeemed)),
2604
- data: {
2605
- assets: args.assets.toString(),
2606
- caller: args.caller.toLowerCase(),
2607
- receiver: args.receiver.toLowerCase(),
2608
- shares: args.earnShares.toString(),
2609
- signer,
2610
- status: 'completed',
2611
- vault: redeemed.address.toLowerCase(),
2612
- },
2613
- type: 'shares-redeemed',
2834
+ type: 'shares-redeemed',
2835
+ };
2836
+ });
2837
+ recognize(named('WithdrewExact'), (event) => {
2838
+ const args = event.args;
2839
+ claimVaultLegs(event.address, [args.assets, args.earnSharesBurned]);
2840
+ return {
2841
+ ...itemBase(event.logIndex),
2842
+ data: {
2843
+ assets: args.assets.toString(),
2844
+ caller: lc(args.caller),
2845
+ receiver: lc(args.receiver),
2846
+ sharesBurned: args.earnSharesBurned.toString(),
2847
+ signer,
2848
+ status: 'completed',
2849
+ vault: lc(event.address),
2614
2850
  },
2615
- ];
2616
- }
2617
- const withdrew = finalEvents.find((event) => event.eventName === 'WithdrewExact');
2618
- if (withdrew) {
2619
- const args = withdrew.args;
2620
- return [
2621
- {
2622
- ...itemBase(logIndexOfEvent(withdrew)),
2623
- data: {
2624
- assets: args.assets.toString(),
2625
- caller: args.caller.toLowerCase(),
2626
- sharesBurned: args.earnSharesBurned.toString(),
2627
- receiver: args.receiver.toLowerCase(),
2628
- signer,
2629
- status: 'completed',
2630
- vault: withdrew.address.toLowerCase(),
2631
- },
2632
- type: 'assets-withdrawn',
2851
+ type: 'assets-withdrawn',
2852
+ };
2853
+ });
2854
+ recognize(named('RedeemRequested'), (event) => {
2855
+ const args = event.args;
2856
+ claimVaultLegs(event.address, [args.earnShares]);
2857
+ return {
2858
+ ...itemBase(event.logIndex),
2859
+ data: {
2860
+ receiver: lc(args.receiver),
2861
+ requestId: args.requestId,
2862
+ requester: lc(args.requester),
2863
+ shares: args.earnShares.toString(),
2864
+ signer,
2865
+ status: 'pending',
2866
+ vault: lc(event.address),
2633
2867
  },
2634
- ];
2635
- }
2636
- const requested = finalEvents.find((event) => event.eventName === 'RedeemRequested');
2637
- if (requested) {
2638
- const args = requested.args;
2639
- return [
2640
- {
2641
- ...itemBase(logIndexOfEvent(requested)),
2642
- data: {
2643
- receiver: args.receiver.toLowerCase(),
2644
- requestId: args.requestId,
2645
- requester: args.requester.toLowerCase(),
2646
- shares: args.earnShares.toString(),
2647
- signer,
2648
- status: 'pending',
2649
- vault: requested.address.toLowerCase(),
2650
- },
2651
- type: 'shares-redemption-requested',
2868
+ type: 'shares-redemption-requested',
2869
+ };
2870
+ });
2871
+ recognize(named('RedeemFinalized'), (event) => {
2872
+ const args = event.args;
2873
+ claimVaultLegs(event.address, [args.assets, args.earnShares]);
2874
+ return {
2875
+ ...itemBase(event.logIndex),
2876
+ data: {
2877
+ asset: lc(args.asset),
2878
+ assets: args.assets.toString(),
2879
+ receiver: lc(args.receiver),
2880
+ requestId: args.requestId,
2881
+ shares: args.earnShares.toString(),
2882
+ signer,
2883
+ status: 'completed',
2884
+ vault: lc(event.address),
2652
2885
  },
2653
- ];
2654
- }
2655
- const finalized = finalEvents.find((event) => event.eventName === 'RedeemFinalized');
2656
- if (finalized) {
2657
- const args = finalized.args;
2658
- return [
2659
- {
2660
- ...itemBase(logIndexOfEvent(finalized)),
2661
- data: {
2662
- asset: args.asset.toLowerCase(),
2663
- assets: args.assets.toString(),
2664
- receiver: args.receiver.toLowerCase(),
2665
- requestId: args.requestId,
2666
- shares: args.earnShares.toString(),
2667
- signer,
2668
- status: 'completed',
2669
- vault: finalized.address.toLowerCase(),
2670
- },
2671
- type: 'shares-redemption-finalized',
2886
+ type: 'shares-redemption-finalized',
2887
+ };
2888
+ });
2889
+ recognize(named('RedeemCancelled'), (event) => {
2890
+ const args = event.args;
2891
+ claimVaultLegs(event.address, [args.earnShares]);
2892
+ return {
2893
+ ...itemBase(event.logIndex),
2894
+ data: {
2895
+ receiver: lc(args.receiver),
2896
+ requestId: args.requestId,
2897
+ shares: args.earnShares.toString(),
2898
+ signer,
2899
+ status: 'cancelled',
2900
+ vault: lc(event.address),
2672
2901
  },
2673
- ];
2674
- }
2675
- const cancelled = finalEvents.find((event) => event.eventName === 'RedeemCancelled');
2676
- if (cancelled) {
2677
- const args = cancelled.args;
2678
- return [
2679
- {
2680
- ...itemBase(logIndexOfEvent(cancelled)),
2681
- data: {
2682
- receiver: args.receiver.toLowerCase(),
2683
- requestId: args.requestId,
2684
- shares: args.earnShares.toString(),
2685
- signer,
2686
- status: 'cancelled',
2687
- vault: cancelled.address.toLowerCase(),
2688
- },
2689
- type: 'shares-redemption-cancelled',
2902
+ type: 'shares-redemption-cancelled',
2903
+ };
2904
+ });
2905
+ // External ERC-4626 vaults. Claim the share and asset legs unconditionally
2906
+ // so nested vault plumbing never resurfaces as burns or transfers, but emit
2907
+ // only operations the viewer is a party to. Fold-only recognitions are
2908
+ // buffered and surface only when nothing else classified.
2909
+ const suppressedVaultItems = [];
2910
+ const viewerInvolved = (parties, legs) => parties.some((party) => Address.isEqual(party, viewer)) ||
2911
+ legs.some((leg) => isTransfer(leg) &&
2912
+ (Address.isEqual(transferArgs(leg).from, viewer) ||
2913
+ Address.isEqual(transferArgs(leg).to, viewer)));
2914
+ recognize(named('Withdraw'), (event) => {
2915
+ const args = event.args;
2916
+ const vault = event.address;
2917
+ // Parse before claiming so a malformed event consumes nothing but itself.
2918
+ const data = {
2919
+ assets: args.assets.toString(),
2920
+ caller: lc(args.sender),
2921
+ receiver: lc(args.receiver),
2922
+ sharesBurned: args.shares.toString(),
2923
+ signer,
2924
+ status: 'completed',
2925
+ vault: lc(vault),
2926
+ };
2927
+ const legs = claimVaultLegs(vault, [args.assets, args.shares]);
2928
+ const item = { ...itemBase(event.logIndex), data, type: 'assets-withdrawn' };
2929
+ if (viewerInvolved([args.sender, args.receiver, args.owner], legs))
2930
+ return item;
2931
+ suppressedVaultItems.push(item);
2932
+ return undefined;
2933
+ });
2934
+ recognize(named('Deposit'), (event) => {
2935
+ const args = event.args;
2936
+ const vault = event.address;
2937
+ // Parse before claiming so a malformed event consumes nothing but itself.
2938
+ const data = {
2939
+ assets: args.assets.toString(),
2940
+ caller: lc(args.sender),
2941
+ receiver: lc(args.owner),
2942
+ shares: args.shares.toString(),
2943
+ signer,
2944
+ status: 'completed',
2945
+ vault: lc(vault),
2946
+ };
2947
+ const legs = claimVaultLegs(vault, [args.assets, args.shares]);
2948
+ const item = { ...itemBase(event.logIndex), data, type: 'assets-deposited' };
2949
+ if (viewerInvolved([args.sender, args.owner], legs))
2950
+ return item;
2951
+ suppressedVaultItems.push(item);
2952
+ return undefined;
2953
+ });
2954
+ // DEX order placed: `OrderPlaced` carries the escrow token/amount and side;
2955
+ // claim the escrow transfer that funded it.
2956
+ const orderPlacedItem = (event) => {
2957
+ const args = event.args;
2958
+ const escrow = (leg) => isTransfer(leg) &&
2959
+ Address.isEqual(leg.address, args.token) &&
2960
+ Address.isEqual(transferArgs(leg).to, event.address);
2961
+ // The escrow funds the placement, so it is the nearest preceding match; a
2962
+ // same-token earlier swap input must not be taken instead.
2963
+ const escrowLeg = unclaimed((leg) => escrow(leg) && leg.logIndex < event.logIndex && transferArgs(leg).amount === args.amount).at(-1) ?? unclaimed((leg) => escrow(leg) && leg.logIndex < event.logIndex).at(-1);
2964
+ if (escrowLeg)
2965
+ claim(escrowLeg);
2966
+ return {
2967
+ ...itemBase(event.logIndex),
2968
+ data: {
2969
+ orderId: args.orderId.toString(),
2970
+ side: args.isBid ? 'bid' : 'ask',
2971
+ signer,
2972
+ sourceToken: token(args.amount, args.token),
2973
+ tick: Number(args.tick),
2690
2974
  },
2691
- ];
2692
- }
2693
- // Swap: a transfer into the DEX paired with one out of the DEX.
2694
- const toDex = transfers.find((e) => Address.isEqual(e.args.to, Addresses.stablecoinDex));
2695
- const fromDex = transfers.find((e) => Address.isEqual(e.args.from, Addresses.stablecoinDex));
2696
- if (toDex && fromDex) {
2697
- const swapper = fromDex.args.to;
2698
- const destTokenAddress = fromDex.address;
2699
- const sourceToken = token(toDex.args.amount, toDex.address);
2700
- // Cross-token transfer: the swapped-out token is forwarded from the swapper
2701
- // to a different recipient in the same transaction (the account server's
2702
- // approve+swap+transfer multicall). Fold the swap and its delivery leg into
2703
- // one `transfer` carrying both the source token spent into the DEX and the
2704
- // destination token delivered to the recipient. A swap whose output stays
2705
- // with the swapper (no onward leg) stays a plain `swap` below.
2706
- const delivery = transfers.find((e) => Address.isEqual(e.address, destTokenAddress) &&
2707
- Address.isEqual(e.args.from, swapper) &&
2708
- !Address.isEqual(e.args.to, Addresses.stablecoinDex) &&
2709
- !Address.isEqual(e.args.to, swapper));
2710
- if (delivery) {
2711
- const args = delivery.args;
2712
- const rawMemo = delivery.eventName === 'TransferWithMemo' ? args.memo : undefined;
2713
- const memo = rawMemo ? Transfers.memoToString(rawMemo) : undefined;
2714
- const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo });
2715
- return [
2716
- transfer(itemBase(logIndexOfEvent(delivery)), blockNum, {
2717
- attribution,
2718
- destinationToken: token(args.amount, delivery.address),
2719
- direction: Address.isEqual(args.to, viewer) ? 'in' : 'out',
2720
- memo,
2721
- recipient: args.to.toLowerCase(),
2722
- sender: swapper.toLowerCase(),
2723
- signer,
2724
- sourceToken,
2725
- }),
2726
- ];
2975
+ type: 'order-placed',
2976
+ };
2977
+ };
2978
+ const isFlipOrder = (event) => 'isFlipOrder' in event.args && event.args.isFlipOrder === true;
2979
+ // Non-flip order escrow legs claim before swap pairing so they cannot
2980
+ // mispair with an unrelated swap's output leg.
2981
+ recognize((event) => event.eventName === 'OrderPlaced' && !isFlipOrder(event), orderPlacedItem);
2982
+ // Swap: a transfer into a route paired with a different token coming back
2983
+ // out. The DEX matches any swapper; other routes must be viewer-bounded
2984
+ // with route-emitted logs between the legs.
2985
+ {
2986
+ // Outgoing transfers indexed by lowercase sender, for O(candidates)
2987
+ // destination lookups. Claimed entries are skipped at scan time.
2988
+ const transfersByFrom = new Map();
2989
+ for (const event of pool) {
2990
+ if (!isTransfer(event))
2991
+ continue;
2992
+ const key = transferArgs(event).from.toLowerCase();
2993
+ const list = transfersByFrom.get(key);
2994
+ if (list)
2995
+ list.push(event);
2996
+ else
2997
+ transfersByFrom.set(key, [event]);
2998
+ }
2999
+ // One forward pass: claims only shrink the pool, so a source that finds
3000
+ // no destination now never finds one later, and scanning each source once
3001
+ // keeps the cost linear in sources instead of restarting per pair.
3002
+ for (const source of unclaimed(isTransfer)) {
3003
+ if (claimed.has(source.logIndex))
3004
+ continue;
3005
+ const sourceArgs = transferArgs(source);
3006
+ const route = sourceArgs.to;
3007
+ const routeKey = route.toLowerCase();
3008
+ // Tempo system addresses (the reserve, fee manager, outbox, and every
3009
+ // other non-DEX precompile) are never swap routes; new precompiles join
3010
+ // the exclusion automatically.
3011
+ if (nonRouteSystemAddresses.has(routeKey))
3012
+ continue;
3013
+ if (Address.isEqual(route, zeroAddress))
3014
+ continue;
3015
+ if (Address.isEqual(route, viewer))
3016
+ continue;
3017
+ const isDex = Address.isEqual(route, Addresses.stablecoinDex);
3018
+ if (!isDex && !Address.isEqual(sourceArgs.from, viewer))
3019
+ continue;
3020
+ const candidates = (transfersByFrom.get(routeKey) ?? []).filter((event) => {
3021
+ if (claimed.has(event.logIndex) || event.logIndex <= source.logIndex)
3022
+ return false;
3023
+ if (Address.isEqual(event.address, source.address))
3024
+ return false;
3025
+ return !Address.isEqual(transferArgs(event).to, Addresses.feeManager);
3026
+ });
3027
+ // Prefer the output paying the swapper back (so interleaved swaps by
3028
+ // different accounts keep their own outputs); otherwise a DEX pairs
3029
+ // first-in-first-out, and a route's last leg is a direct delivery
3030
+ // (earlier different-token legs are intermediate hops).
3031
+ const destination = candidates.find((event) => Address.isEqual(transferArgs(event).to, sourceArgs.from)) ??
3032
+ (isDex ? candidates[0] : candidates.at(-1));
3033
+ if (!destination)
3034
+ continue;
3035
+ // Route evidence for non-precompile routes: the route itself emitted a
3036
+ // log between the legs (raw rows, so undecoded router events count). A
3037
+ // counterparty that merely received and later paid back is not a route.
3038
+ if (!isDex) {
3039
+ const evidence = rows.some((r) => r.logIdx > source.logIndex &&
3040
+ r.logIdx < destination.logIndex &&
3041
+ r.logAddress.toLowerCase() === routeKey);
3042
+ if (!evidence)
3043
+ continue;
3044
+ }
3045
+ journal = [];
3046
+ try {
3047
+ claim(source);
3048
+ claim(destination);
3049
+ const destinationArgs = transferArgs(destination);
3050
+ const swapper = isDex ? destinationArgs.to : sourceArgs.from;
3051
+ const destTokenAddress = destination.address;
3052
+ const sourceToken = token(sourceArgs.amount, source.address);
3053
+ // Router plumbing between the legs: route-touching or zero-address
3054
+ // token moves and router-side approvals, never viewer-party transfers.
3055
+ // DEX-precompile pairs claim nothing here, so an interleaved swap by
3056
+ // another account keeps its own legs.
3057
+ if (!isDex)
3058
+ for (const event of unclaimed()) {
3059
+ if (event.logIndex <= source.logIndex || event.logIndex >= destination.logIndex)
3060
+ continue;
3061
+ if (isTransfer(event)) {
3062
+ const args = transferArgs(event);
3063
+ if (Address.isEqual(args.from, viewer) || Address.isEqual(args.to, viewer))
3064
+ continue;
3065
+ if (Address.isEqual(args.from, route) ||
3066
+ Address.isEqual(args.to, route) ||
3067
+ Address.isEqual(args.from, zeroAddress) ||
3068
+ Address.isEqual(args.to, zeroAddress))
3069
+ claim(event);
3070
+ }
3071
+ else if (event.eventName === 'Approval' &&
3072
+ 'owner' in event.args &&
3073
+ !Address.isEqual(event.args.owner, viewer))
3074
+ claim(event);
3075
+ }
3076
+ // The allowance granted for this swap: same token, owner, and amount,
3077
+ // and causally tied to the input leg (spender is the route, or the
3078
+ // approval immediately precedes it). Unrelated approvals stay.
3079
+ unclaimed((event) => event.eventName === 'Approval' &&
3080
+ 'amount' in event.args &&
3081
+ 'owner' in event.args &&
3082
+ 'spender' in event.args &&
3083
+ Address.isEqual(event.address, source.address) &&
3084
+ Address.isEqual(event.args.owner, sourceArgs.from) &&
3085
+ event.args.amount === sourceArgs.amount &&
3086
+ event.logIndex < source.logIndex &&
3087
+ (Address.isEqual(event.args.spender, route) ||
3088
+ event.logIndex === source.logIndex - 1))
3089
+ .slice(-1)
3090
+ .forEach(claim);
3091
+ // DEX bookkeeping this swap produced: fills against resting orders and
3092
+ // the auto-flip re-placement, acted by the swapper or its route account.
3093
+ // A fill of the viewer's own resting order stays its own item.
3094
+ const isBookkeepingActor = (value) => Address.isEqual(value, swapper) || Address.isEqual(value, route);
3095
+ unclaimed((event) => (Address.isEqual(event.address, route) ||
3096
+ Address.isEqual(event.address, Addresses.stablecoinDex)) &&
3097
+ ((event.eventName === 'OrderFilled' &&
3098
+ 'taker' in event.args &&
3099
+ isBookkeepingActor(event.args.taker) &&
3100
+ !('maker' in event.args &&
3101
+ Address.isEqual(event.args.maker, viewer))) ||
3102
+ (event.eventName === 'OrderPlaced' &&
3103
+ isFlipOrder(event) &&
3104
+ 'maker' in event.args &&
3105
+ isBookkeepingActor(event.args.maker)))).forEach(claim);
3106
+ // Cross-token transfer: the swapped-out token is forwarded whole from
3107
+ // the swapper to a new recipient after the swap (the account server's
3108
+ // approve+swap+transfer multicall), or the route delivered it to the
3109
+ // recipient directly; fold both shapes into one `transfer`. A leg
3110
+ // whose recipient later forwards a different token is the input of a
3111
+ // chained swap, not a delivery.
3112
+ const forwardsOn = (candidate) => {
3113
+ const args = transferArgs(candidate);
3114
+ return (transfersByFrom.get(args.to.toLowerCase()) ?? []).some((onward) => !claimed.has(onward.logIndex) &&
3115
+ onward.logIndex > candidate.logIndex &&
3116
+ !Address.isEqual(onward.address, candidate.address));
3117
+ };
3118
+ const delivery = !isDex && !Address.isEqual(destinationArgs.to, viewer)
3119
+ ? destination
3120
+ : takeTransfer((event) => {
3121
+ const args = transferArgs(event);
3122
+ return (event.logIndex > destination.logIndex &&
3123
+ Address.isEqual(event.address, destTokenAddress) &&
3124
+ Address.isEqual(args.from, swapper) &&
3125
+ args.amount === destinationArgs.amount &&
3126
+ !Address.isEqual(args.to, route) &&
3127
+ !Address.isEqual(args.to, swapper) &&
3128
+ !forwardsOn(event));
3129
+ });
3130
+ // A malformed delivery memo degrades to a plain swap item; the claimed
3131
+ // legs must still produce an activity.
3132
+ const item = (() => {
3133
+ if (!delivery)
3134
+ return undefined;
3135
+ try {
3136
+ const args = transferArgs(delivery);
3137
+ const rawMemo = delivery.eventName === 'TransferWithMemo' ? args.memo : undefined;
3138
+ const memo = rawMemo ? Transfers.memoToString(rawMemo) : undefined;
3139
+ const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo });
3140
+ return transfer(itemBase(delivery.logIndex), blockNum, {
3141
+ attribution,
3142
+ destinationToken: token(args.amount, delivery.address),
3143
+ direction: Address.isEqual(args.to, viewer) ? 'in' : 'out',
3144
+ memo,
3145
+ recipient: lc(args.to),
3146
+ sender: lc(swapper),
3147
+ signer,
3148
+ sourceToken,
3149
+ });
3150
+ }
3151
+ catch {
3152
+ return undefined;
3153
+ }
3154
+ })();
3155
+ items.push(item ?? {
3156
+ ...itemBase(source.logIndex),
3157
+ data: {
3158
+ destinationToken: token(destinationArgs.amount, destTokenAddress),
3159
+ signer,
3160
+ sourceToken,
3161
+ },
3162
+ type: 'swap',
3163
+ });
3164
+ }
3165
+ catch {
3166
+ // A malformed leg costs only this pair; release its claims so the
3167
+ // legs fall through to the later recognizers.
3168
+ for (const index of journal ?? [])
3169
+ claimed.delete(index);
3170
+ }
3171
+ finally {
3172
+ journal = undefined;
3173
+ }
2727
3174
  }
2728
- return [
2729
- {
2730
- ...itemBase(Math.min(logIndexOfEvent(toDex), logIndexOfEvent(fromDex))),
2731
- data: {
2732
- destinationToken: token(fromDex.args.amount, destTokenAddress),
2733
- signer,
2734
- sourceToken,
2735
- },
2736
- type: 'swap',
2737
- },
2738
- ];
2739
- }
2740
- // Mint: explicit `Mint` event (not the fee manager) or a transfer from zero.
2741
- const mintEvent = finalEvents.find((e) => e.eventName === 'Mint' &&
2742
- 'amount' in e.args &&
2743
- !Address.isEqual(e.address, Addresses.feeManager));
2744
- if (mintEvent) {
2745
- const args = mintEvent.args;
2746
- return [
2747
- {
2748
- ...itemBase(logIndexOfEvent(mintEvent)),
2749
- data: {
2750
- recipient: args.to.toLowerCase(),
2751
- sourceToken: token(args.amount, mintEvent.address),
2752
- },
2753
- type: 'mint',
2754
- },
2755
- ];
2756
- }
2757
- const zeroTransferIn = transfers.find((e) => Address.isEqual(e.args.from, zeroAddress));
2758
- if (zeroTransferIn) {
2759
- const args = zeroTransferIn.args;
2760
- return [
2761
- {
2762
- ...itemBase(logIndexOfEvent(zeroTransferIn)),
2763
- data: {
2764
- recipient: args.to.toLowerCase(),
2765
- sourceToken: token(args.amount, zeroTransferIn.address),
2766
- },
2767
- type: 'mint',
2768
- },
2769
- ];
2770
3175
  }
3176
+ // Mint: explicit `Mint` events (not the fee manager) and remaining transfers
3177
+ // from the zero address (the dedup above already folded event/transfer pairs).
3178
+ const mintItem = (event, args) => ({
3179
+ ...itemBase(event.logIndex),
3180
+ data: {
3181
+ recipient: lc(args.to),
3182
+ sourceToken: token(args.amount, event.address),
3183
+ },
3184
+ type: 'mint',
3185
+ });
3186
+ recognize((event) => event.eventName === 'Mint' &&
3187
+ 'amount' in event.args &&
3188
+ 'to' in event.args &&
3189
+ !Address.isEqual(event.address, Addresses.feeManager), (event) => mintItem(event, event.args));
3190
+ recognize((event) => isTransfer(event) &&
3191
+ Address.isEqual(transferArgs(event).from, zeroAddress), (event) => mintItem(event, transferArgs(event)));
2771
3192
  // Burn blocked: a compliance burn of receive-policy-blocked funds. It also
2772
- // emits a zero-address `Transfer` that would otherwise classify as `burn`, so
2773
- // detect the distinct `BurnBlocked` event first.
2774
- const burnBlockedEvent = finalEvents.find((e) => e.eventName === 'BurnBlocked');
2775
- if (burnBlockedEvent) {
2776
- const args = burnBlockedEvent.args;
2777
- return [
2778
- {
2779
- ...itemBase(logIndexOfEvent(burnBlockedEvent)),
2780
- data: {
2781
- sender: args.from.toLowerCase(),
2782
- signer,
2783
- sourceToken: token(args.amount, burnBlockedEvent.address),
2784
- },
2785
- type: 'burn-blocked',
2786
- },
2787
- ];
2788
- }
2789
- // Burn: explicit `Burn` event or a transfer to zero.
2790
- const burnEvent = finalEvents.find((e) => e.eventName === 'Burn' && 'amount' in e.args);
2791
- if (burnEvent) {
2792
- const args = burnEvent.args;
2793
- return [
2794
- {
2795
- ...itemBase(logIndexOfEvent(burnEvent)),
2796
- data: {
2797
- sender: args.from.toLowerCase(),
2798
- signer,
2799
- sourceToken: token(args.amount, burnEvent.address),
2800
- },
2801
- type: 'burn',
2802
- },
2803
- ];
2804
- }
2805
- const zeroTransferOut = transfers.find((e) => Address.isEqual(e.args.to, zeroAddress));
2806
- if (zeroTransferOut) {
2807
- const args = zeroTransferOut.args;
2808
- return [
2809
- {
2810
- ...itemBase(logIndexOfEvent(zeroTransferOut)),
2811
- data: {
2812
- sender: args.from.toLowerCase(),
2813
- signer,
2814
- sourceToken: token(args.amount, zeroTransferOut.address),
2815
- },
2816
- type: 'burn',
3193
+ // emits a zero-address `Transfer`; claim that paired leg so it cannot
3194
+ // classify as a plain `burn`.
3195
+ recognize(named('BurnBlocked'), (event) => {
3196
+ const args = event.args;
3197
+ takeTransfer((leg) => Address.isEqual(leg.address, event.address) &&
3198
+ Address.isEqual(transferArgs(leg).to, zeroAddress) &&
3199
+ transferArgs(leg).amount === args.amount);
3200
+ return {
3201
+ ...itemBase(event.logIndex),
3202
+ data: {
3203
+ sender: lc(args.from),
3204
+ signer,
3205
+ sourceToken: token(args.amount, event.address),
2817
3206
  },
2818
- ];
2819
- }
2820
- // Approval (excluding zone-outbox spenders).
2821
- const approvalEvent = finalEvents.find((e) => e.eventName === 'Approval' &&
3207
+ type: 'burn-blocked',
3208
+ };
3209
+ });
3210
+ // Burn: explicit `Burn` events and remaining transfers to the zero address.
3211
+ const burnItem = (event, args) => ({
3212
+ ...itemBase(event.logIndex),
3213
+ data: {
3214
+ sender: lc(args.from),
3215
+ signer,
3216
+ sourceToken: token(args.amount, event.address),
3217
+ },
3218
+ type: 'burn',
3219
+ });
3220
+ recognize((event) => event.eventName === 'Burn' && 'amount' in event.args && 'from' in event.args, (event) => burnItem(event, event.args));
3221
+ recognize((event) => isTransfer(event) && Address.isEqual(transferArgs(event).to, zeroAddress), (event) => burnItem(event, transferArgs(event)));
3222
+ // Approval (excluding zone-outbox spenders): only viewer-party approvals
3223
+ // surface here; the rest stay for the nothing-else-classified fallback.
3224
+ const isErc20Approval = (event) => {
3225
+ if (event.eventName !== 'Approval')
3226
+ return false;
3227
+ const args = event.args;
3228
+ if (typeof args.spender !== 'string' || !Address.validate(args.spender))
3229
+ return false;
2822
3230
  // ERC-721 uses the same selector but indexes its token ID as topic3. In
2823
3231
  // non-strict mode viem otherwise decodes the missing ERC-20 amount as zero.
2824
- rows.some((row) => row.logIdx === e.logIndex &&
3232
+ return (rows.some((row) => row.logIdx === event.logIndex &&
2825
3233
  row.topic3 === null &&
2826
- /^0x[0-9a-fA-F]{64}$/.test(row.data ?? '')) &&
2827
- !Address.isEqual(e.args.spender, Addresses.zoneOutbox));
2828
- if (approvalEvent) {
2829
- const args = approvalEvent.args;
2830
- return [
2831
- {
2832
- ...itemBase(logIndexOfEvent(approvalEvent)),
2833
- data: {
2834
- signer,
2835
- sourceToken: token(args.amount, approvalEvent.address),
2836
- spender: args.spender.toLowerCase(),
2837
- },
2838
- type: 'approval',
3234
+ /^0x[0-9a-fA-F]{64}$/.test(row.data ?? '')) && !Address.isEqual(args.spender, Addresses.zoneOutbox));
3235
+ };
3236
+ const approvalItem = (event) => {
3237
+ const args = event.args;
3238
+ return {
3239
+ ...itemBase(event.logIndex),
3240
+ data: {
3241
+ signer,
3242
+ sourceToken: token(args.amount, event.address),
3243
+ spender: lc(args.spender),
2839
3244
  },
2840
- ];
2841
- }
2842
- // Precompile-event activities. These run before the generic transfer
2843
- // fallback because each is emitted alongside a co-located TIP-20 `Transfer`
2844
- // whose counterparty is the precompile (the DEX, the channel reserve, the fee
2845
- // manager, the token itself), which would otherwise classify as a plain
2846
- // transfer. They are grouped by (transaction + precompile counterparty), not
2847
- // by log adjacency.
2848
- // Payment channel opened: `ChannelOpened` carries the deposit token/amount
2849
- // directly, so no paired-transfer lookup is needed.
2850
- const channelOpened = finalEvents.find((e) => e.eventName === 'ChannelOpened');
2851
- if (channelOpened) {
2852
- const args = channelOpened.args;
2853
- return [
2854
- {
2855
- ...itemBase(logIndexOfEvent(channelOpened)),
2856
- data: {
2857
- channelId: args.channelId.toLowerCase(),
2858
- payee: args.payee.toLowerCase(),
2859
- payer: args.payer.toLowerCase(),
2860
- signer,
2861
- sourceToken: token(args.deposit, args.token),
2862
- },
2863
- type: 'channel-opened',
3245
+ type: 'approval',
3246
+ };
3247
+ };
3248
+ recognize((event) => isErc20Approval(event) &&
3249
+ 'owner' in event.args &&
3250
+ (Address.isEqual(event.args.owner, viewer) ||
3251
+ Address.isEqual(event.args.spender, viewer)), approvalItem);
3252
+ // Payment channels. The reserve doesn't put the token on `TopUp`/`Settled`/
3253
+ // `ChannelClosed`, so each anchor claims its own reserve leg to recover it;
3254
+ // batched channel operations pair with their own legs one at a time.
3255
+ recognize(named('ChannelOpened'), (event) => {
3256
+ const args = event.args;
3257
+ takeTransfer((leg) => Address.isEqual(leg.address, args.token) &&
3258
+ Address.isEqual(transferArgs(leg).to, tip20ChannelReserve) &&
3259
+ transferArgs(leg).amount === args.deposit);
3260
+ return {
3261
+ ...itemBase(event.logIndex),
3262
+ data: {
3263
+ channelId: hx(args.channelId),
3264
+ payee: lc(args.payee),
3265
+ payer: lc(args.payer),
3266
+ signer,
3267
+ sourceToken: token(args.deposit, args.token),
2864
3268
  },
2865
- ];
2866
- }
2867
- // The channel reserve doesn't put the token on `TopUp`/`Settled`/
2868
- // `ChannelClosed`, so recover it from the co-located funding/payout transfer.
2869
- const reserveInflow = transfers.find((e) => Address.isEqual(e.args.to, tip20ChannelReserve));
2870
- const reserveOutflowTo = (recipient) => transfers.find((e) => Address.isEqual(e.args.from, tip20ChannelReserve) &&
2871
- Address.isEqual(e.args.to, recipient));
2872
- // Payment channel topped up: `additionalDeposit` from the event, token from
2873
- // the inbound funding transfer to the reserve.
2874
- const topUp = finalEvents.find((e) => e.eventName === 'TopUp');
2875
- if (topUp) {
2876
- const args = topUp.args;
2877
- const tokenAddress = reserveInflow
2878
- ? reserveInflow.address
3269
+ type: 'channel-opened',
3270
+ };
3271
+ });
3272
+ recognize(named('TopUp'), (event) => {
3273
+ const args = event.args;
3274
+ const inflow = takeTransfer((leg) => Address.isEqual(transferArgs(leg).to, tip20ChannelReserve) &&
3275
+ transferArgs(leg).amount === args.additionalDeposit) ?? takeTransfer((leg) => Address.isEqual(transferArgs(leg).to, tip20ChannelReserve));
3276
+ // A top-up cancels any pending close request on the channel; fold the
3277
+ // cancellation event it bundles.
3278
+ unclaimed((e) => e.eventName === 'CloseRequestCancelled' &&
3279
+ 'channelId' in e.args &&
3280
+ e.args.channelId === args.channelId)
3281
+ .slice(0, 1)
3282
+ .forEach(claim);
3283
+ const tokenAddress = inflow
3284
+ ? inflow.address
2879
3285
  : zeroAddress;
2880
- return [
2881
- {
2882
- ...itemBase(logIndexOfEvent(topUp)),
2883
- data: {
2884
- channelId: args.channelId.toLowerCase(),
2885
- signer,
2886
- sourceToken: token(args.additionalDeposit, tokenAddress),
2887
- },
2888
- type: 'channel-funded',
3286
+ return {
3287
+ ...itemBase(event.logIndex),
3288
+ data: {
3289
+ channelId: hx(args.channelId),
3290
+ signer,
3291
+ sourceToken: token(args.additionalDeposit, tokenAddress),
2889
3292
  },
2890
- ];
2891
- }
2892
- // Payment channel settled: `deltaPaid` from the event, token from the payout
2893
- // transfer out of the reserve to the payee.
2894
- const settled = finalEvents.find((e) => e.eventName === 'Settled');
2895
- if (settled) {
2896
- const args = settled.args;
2897
- const payeeLeg = reserveOutflowTo(args.payee.toLowerCase());
3293
+ type: 'channel-funded',
3294
+ };
3295
+ });
3296
+ recognize(named('Settled'), (event) => {
3297
+ const args = event.args;
3298
+ const payout = (leg) => Address.isEqual(transferArgs(leg).from, tip20ChannelReserve) &&
3299
+ Address.isEqual(transferArgs(leg).to, args.payee);
3300
+ // Prefer the amount-matched payout so batched channel operations sharing
3301
+ // a payee keep their own legs.
3302
+ const payeeLeg = takeTransfer((leg) => payout(leg) && transferArgs(leg).amount === args.deltaPaid) ??
3303
+ takeTransfer(payout);
2898
3304
  const tokenAddress = payeeLeg
2899
3305
  ? payeeLeg.address
2900
3306
  : zeroAddress;
2901
- return [
2902
- {
2903
- ...itemBase(logIndexOfEvent(settled)),
2904
- data: {
2905
- channelId: args.channelId.toLowerCase(),
2906
- payee: args.payee.toLowerCase(),
2907
- payer: args.payer.toLowerCase(),
2908
- signer,
2909
- sourceToken: token(args.deltaPaid, tokenAddress),
2910
- },
2911
- type: 'channel-settled',
2912
- },
2913
- ];
2914
- }
2915
- // Payment channel closed: payout to the payee and refund to the payer arrive
2916
- // as up to two transfers out of the reserve; attach whichever are present.
2917
- const channelClosed = finalEvents.find((e) => e.eventName === 'ChannelClosed');
2918
- if (channelClosed) {
2919
- const args = channelClosed.args;
2920
- const payeeLeg = reserveOutflowTo(args.payee.toLowerCase());
2921
- const payerLeg = reserveOutflowTo(args.payer.toLowerCase());
2922
- return [
2923
- {
2924
- ...itemBase(logIndexOfEvent(channelClosed)),
2925
- data: {
2926
- channelId: args.channelId.toLowerCase(),
2927
- payee: args.payee.toLowerCase(),
2928
- payer: args.payer.toLowerCase(),
2929
- ...(payerLeg && args.refundedToPayer > 0n
2930
- ? { refund: token(args.refundedToPayer, payerLeg.address) }
2931
- : {}),
2932
- signer,
2933
- ...(payeeLeg
2934
- ? { sourceToken: token(args.settledToPayee, payeeLeg.address) }
2935
- : {}),
2936
- },
2937
- type: 'channel-closed',
3307
+ return {
3308
+ ...itemBase(event.logIndex),
3309
+ data: {
3310
+ channelId: hx(args.channelId),
3311
+ payee: lc(args.payee),
3312
+ payer: lc(args.payer),
3313
+ signer,
3314
+ sourceToken: token(args.deltaPaid, tokenAddress),
2938
3315
  },
2939
- ];
2940
- }
2941
- // Payment channel close cancelled: the payer revoked a pending close request.
2942
- // When bundled with a top-up the `TopUp` classifier above wins; on its own
2943
- // this carries no transfer, so the event itself is the signal.
2944
- const closeCancelled = finalEvents.find((e) => e.eventName === 'CloseRequestCancelled');
2945
- if (closeCancelled) {
2946
- const args = closeCancelled.args;
2947
- return [
2948
- {
2949
- ...itemBase(logIndexOfEvent(closeCancelled)),
2950
- data: {
2951
- channelId: args.channelId.toLowerCase(),
2952
- payee: args.payee.toLowerCase(),
2953
- payer: args.payer.toLowerCase(),
2954
- signer,
2955
- },
2956
- type: 'channel-close-cancelled',
3316
+ type: 'channel-settled',
3317
+ };
3318
+ });
3319
+ recognize(named('ChannelClosed'), (event) => {
3320
+ const args = event.args;
3321
+ // A zero settlement or refund has no leg; claiming one would steal it
3322
+ // from a batched sibling operation.
3323
+ const reserveLegTo = (to, amount) => {
3324
+ if (amount === 0n)
3325
+ return undefined;
3326
+ const payout = (leg) => Address.isEqual(transferArgs(leg).from, tip20ChannelReserve) &&
3327
+ Address.isEqual(transferArgs(leg).to, to);
3328
+ return (takeTransfer((leg) => payout(leg) && transferArgs(leg).amount === amount) ??
3329
+ takeTransfer(payout));
3330
+ };
3331
+ const payeeLeg = reserveLegTo(args.payee, args.settledToPayee);
3332
+ const payerLeg = reserveLegTo(args.payer, args.refundedToPayer);
3333
+ return {
3334
+ ...itemBase(event.logIndex),
3335
+ data: {
3336
+ channelId: hx(args.channelId),
3337
+ payee: lc(args.payee),
3338
+ payer: lc(args.payer),
3339
+ ...(payerLeg && args.refundedToPayer > 0n
3340
+ ? { refund: token(args.refundedToPayer, payerLeg.address) }
3341
+ : {}),
3342
+ signer,
3343
+ ...(payeeLeg
3344
+ ? { sourceToken: token(args.settledToPayee, payeeLeg.address) }
3345
+ : {}),
2957
3346
  },
2958
- ];
2959
- }
2960
- // DEX order placed: `OrderPlaced` carries the escrow token/amount and side.
2961
- // (Auto-flip re-places are inside swaps, which already returned above.)
2962
- const orderPlaced = finalEvents.find((e) => e.eventName === 'OrderPlaced');
2963
- if (orderPlaced) {
2964
- const args = orderPlaced.args;
2965
- return [
2966
- {
2967
- ...itemBase(logIndexOfEvent(orderPlaced)),
2968
- data: {
2969
- orderId: args.orderId.toString(),
2970
- side: args.isBid ? 'bid' : 'ask',
2971
- signer,
2972
- sourceToken: token(args.amount, args.token),
2973
- tick: Number(args.tick),
2974
- },
2975
- type: 'order-placed',
3347
+ type: 'channel-closed',
3348
+ };
3349
+ });
3350
+ // Payment channel close cancelled: carries no transfer, so the event itself
3351
+ // is the signal. Cancellations bundled into a top-up were claimed above.
3352
+ recognize(named('CloseRequestCancelled'), (event) => {
3353
+ const args = event.args;
3354
+ return {
3355
+ ...itemBase(event.logIndex),
3356
+ data: {
3357
+ channelId: hx(args.channelId),
3358
+ payee: lc(args.payee),
3359
+ payer: lc(args.payer),
3360
+ signer,
2976
3361
  },
2977
- ];
2978
- }
2979
- // DEX order cancelled: refund is credited to the internal DEX balance, so
2980
- // there is no co-located transfer the `OrderCancelled` event is the signal.
2981
- const orderCancelled = finalEvents.find((e) => e.eventName === 'OrderCancelled');
2982
- if (orderCancelled) {
2983
- const args = orderCancelled.args;
2984
- return [
2985
- {
2986
- ...itemBase(logIndexOfEvent(orderCancelled)),
2987
- data: { orderId: args.orderId.toString(), signer },
2988
- type: 'order-cancelled',
2989
- },
2990
- ];
3362
+ type: 'channel-close-cancelled',
3363
+ };
3364
+ });
3365
+ // Payment-channel close requested: matched on the raw selector (the event
3366
+ // is not in the merged ABI). Runs after the channel recognizers claimed
3367
+ // their own reserve legs, then takes the leftover refund back to the viewer.
3368
+ for (const row of rows.filter((r) => r.selector === closeRequestedSelector)) {
3369
+ const refundLeg = takeTransfer((event) => {
3370
+ const args = transferArgs(event);
3371
+ if (!Address.isEqual(args.to, viewer))
3372
+ return false;
3373
+ return (Address.isEqual(args.from, tip20ChannelReserve) ||
3374
+ Address.isEqual(args.from, row.logAddress));
3375
+ });
3376
+ const refund = refundLeg
3377
+ ? token(transferArgs(refundLeg).amount, refundLeg.address)
3378
+ : undefined;
3379
+ items.push({
3380
+ ...itemBase(row.logIdx),
3381
+ data: { ...(refund ? { refund } : {}), signer },
3382
+ type: 'session-closed',
3383
+ });
2991
3384
  }
2992
- // Fee-AMM rebalance swap: `validatorToken` in, `userToken` out.
2993
- const rebalanceSwap = finalEvents.find((e) => e.eventName === 'RebalanceSwap');
2994
- if (rebalanceSwap) {
2995
- const args = rebalanceSwap.args;
2996
- return [
2997
- {
2998
- ...itemBase(logIndexOfEvent(rebalanceSwap)),
2999
- data: {
3000
- destinationToken: token(args.amountOut, args.userToken),
3001
- signer,
3002
- sourceToken: token(args.amountIn, args.validatorToken),
3003
- swapper: args.swapper.toLowerCase(),
3004
- },
3005
- type: 'fee-rebalance-swap',
3385
+ // Auto-flip order placements not folded into a swap above.
3386
+ recognize(named('OrderPlaced'), orderPlacedItem);
3387
+ // DEX order cancelled: refund is credited to the internal DEX balance, so
3388
+ // there is no co-located transfer; the event itself is the signal.
3389
+ recognize(named('OrderCancelled'), (event) => {
3390
+ const args = event.args;
3391
+ return {
3392
+ ...itemBase(event.logIndex),
3393
+ data: { orderId: args.orderId.toString(), signer },
3394
+ type: 'order-cancelled',
3395
+ };
3396
+ });
3397
+ // Fee-AMM rebalance swap: `validatorToken` in, `userToken` out; claim both
3398
+ // fee-manager legs so neither resurfaces as a plain transfer.
3399
+ recognize(named('RebalanceSwap'), (event) => {
3400
+ const args = event.args;
3401
+ takeTransfer((leg) => Address.isEqual(leg.address, args.validatorToken) &&
3402
+ Address.isEqual(transferArgs(leg).from, args.swapper) &&
3403
+ Address.isEqual(transferArgs(leg).to, Addresses.feeManager) &&
3404
+ transferArgs(leg).amount === args.amountIn);
3405
+ takeTransfer((leg) => Address.isEqual(leg.address, args.userToken) &&
3406
+ Address.isEqual(transferArgs(leg).from, Addresses.feeManager) &&
3407
+ Address.isEqual(transferArgs(leg).to, args.swapper) &&
3408
+ transferArgs(leg).amount === args.amountOut);
3409
+ return {
3410
+ ...itemBase(event.logIndex),
3411
+ data: {
3412
+ destinationToken: token(args.amountOut, args.userToken),
3413
+ signer,
3414
+ sourceToken: token(args.amountIn, args.validatorToken),
3415
+ swapper: lc(args.swapper),
3006
3416
  },
3007
- ];
3008
- }
3009
- // Validator fees distributed.
3010
- const feesDistributed = finalEvents.find((e) => e.eventName === 'FeesDistributed');
3011
- if (feesDistributed) {
3012
- const args = feesDistributed.args;
3013
- return [
3014
- {
3015
- ...itemBase(logIndexOfEvent(feesDistributed)),
3016
- data: {
3017
- signer,
3018
- sourceToken: token(args.amount, args.token),
3019
- validator: args.validator.toLowerCase(),
3020
- },
3021
- type: 'fees-distributed',
3417
+ type: 'fee-rebalance-swap',
3418
+ };
3419
+ });
3420
+ // Validator fees distributed; claim the fee-manager payout leg.
3421
+ recognize(named('FeesDistributed'), (event) => {
3422
+ const args = event.args;
3423
+ takeTransfer((leg) => Address.isEqual(leg.address, args.token) &&
3424
+ Address.isEqual(transferArgs(leg).from, Addresses.feeManager) &&
3425
+ Address.isEqual(transferArgs(leg).to, args.validator) &&
3426
+ transferArgs(leg).amount === args.amount);
3427
+ return {
3428
+ ...itemBase(event.logIndex),
3429
+ data: {
3430
+ signer,
3431
+ sourceToken: token(args.amount, args.token),
3432
+ validator: lc(args.validator),
3022
3433
  },
3023
- ];
3024
- }
3025
- // Reward pool funded: the funded token is the emitting token contract itself.
3026
- const rewardDistributed = finalEvents.find((e) => e.eventName === 'RewardDistributed');
3027
- if (rewardDistributed) {
3028
- const args = rewardDistributed.args;
3029
- return [
3030
- {
3031
- ...itemBase(logIndexOfEvent(rewardDistributed)),
3032
- data: {
3033
- funder: args.funder.toLowerCase(),
3034
- signer,
3035
- sourceToken: token(args.amount, rewardDistributed.address),
3036
- },
3037
- type: 'reward-distributed',
3434
+ type: 'fees-distributed',
3435
+ };
3436
+ });
3437
+ // Reward pool funded: the funded token is the emitting contract itself;
3438
+ // claim the funding transfer.
3439
+ recognize(named('RewardDistributed'), (event) => {
3440
+ const args = event.args;
3441
+ takeTransfer((leg) => Address.isEqual(leg.address, event.address) &&
3442
+ Address.isEqual(transferArgs(leg).from, args.funder) &&
3443
+ Address.isEqual(transferArgs(leg).to, event.address) &&
3444
+ transferArgs(leg).amount === args.amount);
3445
+ return {
3446
+ ...itemBase(event.logIndex),
3447
+ data: {
3448
+ funder: lc(args.funder),
3449
+ signer,
3450
+ sourceToken: token(args.amount, event.address),
3038
3451
  },
3039
- ];
3040
- }
3452
+ type: 'reward-distributed',
3453
+ };
3454
+ });
3041
3455
  // Reward recipient redirected.
3042
- const rewardRecipientSet = finalEvents.find((e) => e.eventName === 'RewardRecipientSet');
3043
- if (rewardRecipientSet) {
3044
- const args = rewardRecipientSet.args;
3045
- return [
3046
- {
3047
- ...itemBase(logIndexOfEvent(rewardRecipientSet)),
3048
- data: {
3049
- holder: args.holder.toLowerCase(),
3050
- recipient: args.recipient.toLowerCase(),
3051
- signer,
3052
- },
3053
- type: 'reward-recipient-set',
3456
+ recognize(named('RewardRecipientSet'), (event) => {
3457
+ const args = event.args;
3458
+ return {
3459
+ ...itemBase(event.logIndex),
3460
+ data: {
3461
+ holder: lc(args.holder),
3462
+ recipient: lc(args.recipient),
3463
+ signer,
3054
3464
  },
3055
- ];
3056
- }
3465
+ type: 'reward-recipient-set',
3466
+ };
3467
+ });
3057
3468
  // Access-key spending limit updated: `newLimit` is carried as the token
3058
3469
  // amount so it enriches with the token's metadata.
3059
- const spendingLimitUpdated = finalEvents.find((e) => e.eventName === 'SpendingLimitUpdated');
3060
- if (spendingLimitUpdated) {
3061
- const args = spendingLimitUpdated.args;
3062
- return [
3063
- {
3064
- ...itemBase(logIndexOfEvent(spendingLimitUpdated)),
3065
- data: {
3066
- account: args.account.toLowerCase(),
3067
- publicKey: args.publicKey.toLowerCase(),
3068
- signer,
3069
- sourceToken: token(args.newLimit, args.token),
3070
- },
3071
- type: 'spending-limit-updated',
3072
- },
3073
- ];
3074
- }
3075
- // Access-key lifecycle events — emitted alongside any co-located transfer.
3076
- const accessKeyItems = [];
3077
- for (const event of finalEvents) {
3078
- if (event.eventName === 'KeyAuthorized' &&
3079
- Address.isEqual(event.address, Addresses.accountKeychain)) {
3080
- const args = event.args;
3081
- accessKeyItems.push({
3082
- ...itemBase(logIndexOfEvent(event)),
3083
- data: {
3084
- account: args.account.toLowerCase(),
3085
- expiry: Number(args.expiry),
3086
- publicKey: args.publicKey.toLowerCase(),
3087
- signatureType: Number(args.signatureType),
3088
- },
3089
- type: 'access-key-created',
3090
- });
3091
- }
3092
- else if (event.eventName === 'KeyRevoked' &&
3093
- Address.isEqual(event.address, Addresses.accountKeychain)) {
3094
- const args = event.args;
3095
- accessKeyItems.push({
3096
- ...itemBase(logIndexOfEvent(event)),
3097
- data: {
3098
- account: args.account.toLowerCase(),
3099
- publicKey: args.publicKey.toLowerCase(),
3100
- },
3101
- type: 'access-key-revoked',
3102
- });
3103
- }
3104
- }
3105
- // Token creation.
3106
- const tokenCreated = finalEvents.find((e) => e.eventName === 'TokenCreated' && Address.isEqual(e.address, Addresses.tip20Factory));
3107
- if (tokenCreated) {
3108
- const args = tokenCreated.args;
3109
- return [
3110
- {
3111
- ...itemBase(logIndexOfEvent(tokenCreated)),
3112
- data: {
3113
- address: args.token.toLowerCase(),
3114
- currency: args.currency,
3115
- name: args.name,
3116
- symbol: args.symbol,
3117
- },
3118
- type: 'token-created',
3119
- },
3120
- ...accessKeyItems,
3121
- ];
3122
- }
3123
- // Governance / admin / DEX-bookkeeping events. Each maps a single decoded
3124
- // event to a typed item. Checked after token creation (a token-creation tx
3125
- // also emits `RoleMembershipUpdated`) and before the plain-transfer fallback;
3126
- // these carry no token movement, so the event itself is the whole signal.
3127
- const lc = (value) => value.toLowerCase();
3128
- const hx = (value) => value.toLowerCase();
3129
- const findEvent = (name) => finalEvents.find((e) => e.eventName === name);
3130
- // DEX bookkeeping (taker swaps and `OrderPlaced` already returned above).
3131
- const orderFilled = findEvent('OrderFilled');
3132
- if (orderFilled) {
3133
- const a = orderFilled.args;
3134
- return [
3135
- {
3136
- ...itemBase(logIndexOfEvent(orderFilled)),
3137
- data: {
3138
- amountFilled: a.amountFilled.toString(),
3139
- maker: lc(a.maker),
3140
- orderId: a.orderId.toString(),
3141
- partialFill: a.partialFill,
3142
- signer,
3143
- taker: lc(a.taker),
3144
- },
3145
- type: 'order-filled',
3146
- },
3147
- ];
3148
- }
3149
- const orderFlipped = findEvent('OrderFlipped');
3150
- if (orderFlipped) {
3151
- const a = orderFlipped.args;
3152
- return [
3153
- {
3154
- ...itemBase(logIndexOfEvent(orderFlipped)),
3155
- data: {
3156
- flipTick: Number(a.flipTick),
3157
- maker: lc(a.maker),
3158
- orderId: a.orderId.toString(),
3159
- side: a.isBid ? 'bid' : 'ask',
3160
- signer,
3161
- sourceToken: token(a.amount, a.token),
3162
- tick: Number(a.tick),
3163
- },
3164
- type: 'order-flipped',
3165
- },
3166
- ];
3167
- }
3168
- const pairCreated = findEvent('PairCreated');
3169
- if (pairCreated) {
3170
- const a = pairCreated.args;
3171
- return [
3172
- {
3173
- ...itemBase(logIndexOfEvent(pairCreated)),
3174
- data: { base: lc(a.base), key: hx(a.key), quote: lc(a.quote), signer },
3175
- type: 'pair-created',
3176
- },
3177
- ];
3178
- }
3179
- // TIP-20 token configuration / admin.
3180
- const pauseStateUpdate = findEvent('PauseStateUpdate');
3181
- if (pauseStateUpdate) {
3182
- const a = pauseStateUpdate.args;
3183
- return [
3184
- {
3185
- ...itemBase(logIndexOfEvent(pauseStateUpdate)),
3186
- data: { paused: a.isPaused, signer, updater: lc(a.updater) },
3187
- type: 'token-pause-set',
3470
+ recognize(named('SpendingLimitUpdated'), (event) => {
3471
+ const args = event.args;
3472
+ return {
3473
+ ...itemBase(event.logIndex),
3474
+ data: {
3475
+ account: lc(args.account),
3476
+ publicKey: lc(args.publicKey),
3477
+ signer,
3478
+ sourceToken: token(args.newLimit, args.token),
3188
3479
  },
3189
- ];
3190
- }
3191
- const supplyCapUpdate = findEvent('SupplyCapUpdate');
3192
- if (supplyCapUpdate) {
3193
- const a = supplyCapUpdate.args;
3194
- return [
3195
- {
3196
- ...itemBase(logIndexOfEvent(supplyCapUpdate)),
3197
- data: { signer, supplyCap: a.newSupplyCap.toString(), updater: lc(a.updater) },
3198
- type: 'token-supply-cap-set',
3480
+ type: 'spending-limit-updated',
3481
+ };
3482
+ });
3483
+ // Access-key lifecycle events.
3484
+ recognize((event) => event.eventName === 'KeyAuthorized' &&
3485
+ Address.isEqual(event.address, Addresses.accountKeychain), (event) => {
3486
+ const args = event.args;
3487
+ return {
3488
+ ...itemBase(event.logIndex),
3489
+ data: {
3490
+ account: lc(args.account),
3491
+ expiry: Number(args.expiry),
3492
+ publicKey: lc(args.publicKey),
3493
+ signatureType: Number(args.signatureType),
3199
3494
  },
3200
- ];
3201
- }
3202
- const transferPolicyUpdate = findEvent('TransferPolicyUpdate');
3203
- if (transferPolicyUpdate) {
3204
- const a = transferPolicyUpdate.args;
3205
- return [
3206
- {
3207
- ...itemBase(logIndexOfEvent(transferPolicyUpdate)),
3208
- data: { policyId: a.newPolicyId.toString(), signer, updater: lc(a.updater) },
3209
- type: 'token-transfer-policy-set',
3495
+ type: 'access-key-created',
3496
+ };
3497
+ });
3498
+ recognize((event) => event.eventName === 'KeyRevoked' && Address.isEqual(event.address, Addresses.accountKeychain), (event) => {
3499
+ const args = event.args;
3500
+ return {
3501
+ ...itemBase(event.logIndex),
3502
+ data: {
3503
+ account: lc(args.account),
3504
+ publicKey: lc(args.publicKey),
3210
3505
  },
3211
- ];
3212
- }
3213
- const quoteTokenUpdate = findEvent('QuoteTokenUpdate');
3214
- if (quoteTokenUpdate) {
3215
- const a = quoteTokenUpdate.args;
3216
- return [
3217
- {
3218
- ...itemBase(logIndexOfEvent(quoteTokenUpdate)),
3219
- data: { quoteToken: lc(a.newQuoteToken), signer, updater: lc(a.updater) },
3220
- type: 'token-quote-token-set',
3506
+ type: 'access-key-revoked',
3507
+ };
3508
+ });
3509
+ // Token creation. Factory initialization (role grants, config) precedes the
3510
+ // creation event; claim those, leaving later admin actions on the token as
3511
+ // independent activities.
3512
+ recognize((event) => event.eventName === 'TokenCreated' && Address.isEqual(event.address, Addresses.tip20Factory), (event) => {
3513
+ const args = event.args;
3514
+ unclaimed((e) => e.logIndex < event.logIndex && Address.isEqual(e.address, args.token)).forEach(claim);
3515
+ return {
3516
+ ...itemBase(event.logIndex),
3517
+ data: {
3518
+ address: lc(args.token),
3519
+ currency: args.currency,
3520
+ name: args.name,
3521
+ symbol: args.symbol,
3221
3522
  },
3222
- ];
3223
- }
3224
- const nextQuoteTokenSet = findEvent('NextQuoteTokenSet');
3225
- if (nextQuoteTokenSet) {
3226
- const a = nextQuoteTokenSet.args;
3227
- return [
3228
- {
3229
- ...itemBase(logIndexOfEvent(nextQuoteTokenSet)),
3230
- data: { nextQuoteToken: lc(a.nextQuoteToken), signer, updater: lc(a.updater) },
3231
- type: 'token-next-quote-token-set',
3523
+ type: 'token-created',
3524
+ };
3525
+ });
3526
+ // Governance / admin / DEX-bookkeeping events. Each maps a single decoded
3527
+ // event to a typed item; every remaining occurrence emits. These carry no
3528
+ // token movement, so the event itself is the whole signal.
3529
+ recognize(named('OrderFilled'), (event) => {
3530
+ const a = event.args;
3531
+ return {
3532
+ ...itemBase(event.logIndex),
3533
+ data: {
3534
+ amountFilled: a.amountFilled.toString(),
3535
+ maker: lc(a.maker),
3536
+ orderId: a.orderId.toString(),
3537
+ partialFill: a.partialFill,
3538
+ signer,
3539
+ taker: lc(a.taker),
3232
3540
  },
3233
- ];
3234
- }
3235
- const logoUriUpdated = findEvent('LogoURIUpdated');
3236
- if (logoUriUpdated) {
3237
- const a = logoUriUpdated.args;
3238
- return [
3239
- {
3240
- ...itemBase(logIndexOfEvent(logoUriUpdated)),
3241
- data: { logoUri: a.newLogoURI, signer, updater: lc(a.updater) },
3242
- type: 'token-logo-set',
3541
+ type: 'order-filled',
3542
+ };
3543
+ });
3544
+ recognize(named('OrderFlipped'), (event) => {
3545
+ const a = event.args;
3546
+ return {
3547
+ ...itemBase(event.logIndex),
3548
+ data: {
3549
+ flipTick: Number(a.flipTick),
3550
+ maker: lc(a.maker),
3551
+ orderId: a.orderId.toString(),
3552
+ side: a.isBid ? 'bid' : 'ask',
3553
+ signer,
3554
+ sourceToken: token(a.amount, a.token),
3555
+ tick: Number(a.tick),
3243
3556
  },
3244
- ];
3245
- }
3246
- const roleMembershipUpdated = findEvent('RoleMembershipUpdated');
3247
- if (roleMembershipUpdated) {
3248
- const a = roleMembershipUpdated.args;
3249
- return [
3250
- {
3251
- ...itemBase(logIndexOfEvent(roleMembershipUpdated)),
3252
- data: {
3253
- account: lc(a.account),
3254
- granted: a.hasRole,
3255
- role: hx(a.role),
3256
- sender: lc(a.sender),
3257
- signer,
3258
- },
3259
- type: 'role-membership-set',
3557
+ type: 'order-flipped',
3558
+ };
3559
+ });
3560
+ recognize(named('PairCreated'), (event) => {
3561
+ const a = event.args;
3562
+ return {
3563
+ ...itemBase(event.logIndex),
3564
+ data: { base: lc(a.base), key: hx(a.key), quote: lc(a.quote), signer },
3565
+ type: 'pair-created',
3566
+ };
3567
+ });
3568
+ // TIP-20 token configuration / admin.
3569
+ recognize(named('PauseStateUpdate'), (event) => {
3570
+ const a = event.args;
3571
+ return {
3572
+ ...itemBase(event.logIndex),
3573
+ data: { paused: a.isPaused, signer, updater: lc(a.updater) },
3574
+ type: 'token-pause-set',
3575
+ };
3576
+ });
3577
+ recognize(named('SupplyCapUpdate'), (event) => {
3578
+ const a = event.args;
3579
+ return {
3580
+ ...itemBase(event.logIndex),
3581
+ data: { signer, supplyCap: a.newSupplyCap.toString(), updater: lc(a.updater) },
3582
+ type: 'token-supply-cap-set',
3583
+ };
3584
+ });
3585
+ recognize(named('TransferPolicyUpdate'), (event) => {
3586
+ const a = event.args;
3587
+ return {
3588
+ ...itemBase(event.logIndex),
3589
+ data: { policyId: a.newPolicyId.toString(), signer, updater: lc(a.updater) },
3590
+ type: 'token-transfer-policy-set',
3591
+ };
3592
+ });
3593
+ recognize(named('QuoteTokenUpdate'), (event) => {
3594
+ const a = event.args;
3595
+ return {
3596
+ ...itemBase(event.logIndex),
3597
+ data: { quoteToken: lc(a.newQuoteToken), signer, updater: lc(a.updater) },
3598
+ type: 'token-quote-token-set',
3599
+ };
3600
+ });
3601
+ recognize(named('NextQuoteTokenSet'), (event) => {
3602
+ const a = event.args;
3603
+ return {
3604
+ ...itemBase(event.logIndex),
3605
+ data: { nextQuoteToken: lc(a.nextQuoteToken), signer, updater: lc(a.updater) },
3606
+ type: 'token-next-quote-token-set',
3607
+ };
3608
+ });
3609
+ recognize(named('LogoURIUpdated'), (event) => {
3610
+ const a = event.args;
3611
+ return {
3612
+ ...itemBase(event.logIndex),
3613
+ data: { logoUri: a.newLogoURI, signer, updater: lc(a.updater) },
3614
+ type: 'token-logo-set',
3615
+ };
3616
+ });
3617
+ recognize(named('RoleMembershipUpdated'), (event) => {
3618
+ const a = event.args;
3619
+ return {
3620
+ ...itemBase(event.logIndex),
3621
+ data: {
3622
+ account: lc(a.account),
3623
+ granted: a.hasRole,
3624
+ role: hx(a.role),
3625
+ sender: lc(a.sender),
3626
+ signer,
3260
3627
  },
3261
- ];
3262
- }
3263
- const roleAdminUpdated = findEvent('RoleAdminUpdated');
3264
- if (roleAdminUpdated) {
3265
- const a = roleAdminUpdated.args;
3266
- return [
3267
- {
3268
- ...itemBase(logIndexOfEvent(roleAdminUpdated)),
3269
- data: {
3270
- newAdminRole: hx(a.newAdminRole),
3271
- role: hx(a.role),
3272
- sender: lc(a.sender),
3273
- signer,
3274
- },
3275
- type: 'role-admin-set',
3628
+ type: 'role-membership-set',
3629
+ };
3630
+ });
3631
+ recognize(named('RoleAdminUpdated'), (event) => {
3632
+ const a = event.args;
3633
+ return {
3634
+ ...itemBase(event.logIndex),
3635
+ data: {
3636
+ newAdminRole: hx(a.newAdminRole),
3637
+ role: hx(a.role),
3638
+ sender: lc(a.sender),
3639
+ signer,
3276
3640
  },
3277
- ];
3278
- }
3641
+ type: 'role-admin-set',
3642
+ };
3643
+ });
3279
3644
  // Fee-manager token configuration.
3280
- const userTokenSet = findEvent('UserTokenSet');
3281
- if (userTokenSet) {
3282
- const a = userTokenSet.args;
3283
- return [
3284
- {
3285
- ...itemBase(logIndexOfEvent(userTokenSet)),
3286
- data: { signer, token: lc(a.token), user: lc(a.user) },
3287
- type: 'fee-user-token-set',
3288
- },
3289
- ];
3290
- }
3291
- const validatorTokenSet = findEvent('ValidatorTokenSet');
3292
- if (validatorTokenSet) {
3293
- const a = validatorTokenSet.args;
3294
- return [
3295
- {
3296
- ...itemBase(logIndexOfEvent(validatorTokenSet)),
3297
- data: { signer, token: lc(a.token), validator: lc(a.validator) },
3298
- type: 'fee-validator-token-set',
3299
- },
3300
- ];
3301
- }
3302
- // TIP-403 policy registry (create wins over the member events it bundles).
3303
- const compoundPolicyCreated = findEvent('CompoundPolicyCreated');
3304
- if (compoundPolicyCreated) {
3305
- const a = compoundPolicyCreated.args;
3306
- return [
3307
- {
3308
- ...itemBase(logIndexOfEvent(compoundPolicyCreated)),
3309
- data: {
3310
- creator: lc(a.creator),
3311
- mintRecipientPolicyId: a.mintRecipientPolicyId.toString(),
3312
- policyId: a.policyId.toString(),
3313
- recipientPolicyId: a.recipientPolicyId.toString(),
3314
- senderPolicyId: a.senderPolicyId.toString(),
3315
- signer,
3316
- },
3317
- type: 'compound-policy-created',
3645
+ recognize(named('UserTokenSet'), (event) => {
3646
+ const a = event.args;
3647
+ return {
3648
+ ...itemBase(event.logIndex),
3649
+ data: { signer, token: lc(a.token), user: lc(a.user) },
3650
+ type: 'fee-user-token-set',
3651
+ };
3652
+ });
3653
+ recognize(named('ValidatorTokenSet'), (event) => {
3654
+ const a = event.args;
3655
+ return {
3656
+ ...itemBase(event.logIndex),
3657
+ data: { signer, token: lc(a.token), validator: lc(a.validator) },
3658
+ type: 'fee-validator-token-set',
3659
+ };
3660
+ });
3661
+ // TIP-403 policy registry. Creation events claim the member events they
3662
+ // bundle (which precede the anchor); later member updates stay independent.
3663
+ recognize(named('CompoundPolicyCreated'), (event) => {
3664
+ const a = event.args;
3665
+ const componentIds = new Set([a.mintRecipientPolicyId, a.recipientPolicyId, a.senderPolicyId].map(String));
3666
+ unclaimed((e) => e.logIndex < event.logIndex &&
3667
+ Address.isEqual(e.address, event.address) &&
3668
+ ['BlacklistUpdated', 'PolicyAdminUpdated', 'PolicyCreated', 'WhitelistUpdated'].includes(e.eventName) &&
3669
+ 'policyId' in e.args &&
3670
+ componentIds.has(String(e.args.policyId))).forEach(claim);
3671
+ return {
3672
+ ...itemBase(event.logIndex),
3673
+ data: {
3674
+ creator: lc(a.creator),
3675
+ mintRecipientPolicyId: a.mintRecipientPolicyId.toString(),
3676
+ policyId: a.policyId.toString(),
3677
+ recipientPolicyId: a.recipientPolicyId.toString(),
3678
+ senderPolicyId: a.senderPolicyId.toString(),
3679
+ signer,
3318
3680
  },
3319
- ];
3320
- }
3321
- const policyCreated = findEvent('PolicyCreated');
3322
- if (policyCreated) {
3323
- const a = policyCreated.args;
3324
- return [
3325
- {
3326
- ...itemBase(logIndexOfEvent(policyCreated)),
3327
- data: {
3328
- policyId: a.policyId.toString(),
3329
- policyType: Number(a.policyType),
3330
- signer,
3331
- updater: lc(a.updater),
3332
- },
3333
- type: 'policy-created',
3681
+ type: 'compound-policy-created',
3682
+ };
3683
+ });
3684
+ recognize(named('PolicyCreated'), (event) => {
3685
+ const a = event.args;
3686
+ unclaimed((e) => e.logIndex < event.logIndex &&
3687
+ Address.isEqual(e.address, event.address) &&
3688
+ ['BlacklistUpdated', 'PolicyAdminUpdated', 'WhitelistUpdated'].includes(e.eventName) &&
3689
+ 'policyId' in e.args &&
3690
+ e.args.policyId === a.policyId).forEach(claim);
3691
+ return {
3692
+ ...itemBase(event.logIndex),
3693
+ data: {
3694
+ policyId: a.policyId.toString(),
3695
+ policyType: Number(a.policyType),
3696
+ signer,
3697
+ updater: lc(a.updater),
3334
3698
  },
3335
- ];
3336
- }
3337
- const policyAdminUpdated = findEvent('PolicyAdminUpdated');
3338
- if (policyAdminUpdated) {
3339
- const a = policyAdminUpdated.args;
3340
- return [
3341
- {
3342
- ...itemBase(logIndexOfEvent(policyAdminUpdated)),
3343
- data: {
3344
- admin: lc(a.admin),
3345
- policyId: a.policyId.toString(),
3346
- signer,
3347
- updater: lc(a.updater),
3348
- },
3349
- type: 'policy-admin-set',
3699
+ type: 'policy-created',
3700
+ };
3701
+ });
3702
+ recognize(named('PolicyAdminUpdated'), (event) => {
3703
+ const a = event.args;
3704
+ return {
3705
+ ...itemBase(event.logIndex),
3706
+ data: {
3707
+ admin: lc(a.admin),
3708
+ policyId: a.policyId.toString(),
3709
+ signer,
3710
+ updater: lc(a.updater),
3350
3711
  },
3351
- ];
3352
- }
3353
- const whitelistUpdated = findEvent('WhitelistUpdated');
3354
- if (whitelistUpdated) {
3355
- const a = whitelistUpdated.args;
3356
- return [
3357
- {
3358
- ...itemBase(logIndexOfEvent(whitelistUpdated)),
3359
- data: {
3360
- account: lc(a.account),
3361
- allowed: a.allowed,
3362
- policyId: a.policyId.toString(),
3363
- signer,
3364
- updater: lc(a.updater),
3365
- },
3366
- type: 'whitelist-updated',
3712
+ type: 'policy-admin-set',
3713
+ };
3714
+ });
3715
+ recognize(named('WhitelistUpdated'), (event) => {
3716
+ const a = event.args;
3717
+ return {
3718
+ ...itemBase(event.logIndex),
3719
+ data: {
3720
+ account: lc(a.account),
3721
+ allowed: a.allowed,
3722
+ policyId: a.policyId.toString(),
3723
+ signer,
3724
+ updater: lc(a.updater),
3367
3725
  },
3368
- ];
3369
- }
3370
- const blacklistUpdated = findEvent('BlacklistUpdated');
3371
- if (blacklistUpdated) {
3372
- const a = blacklistUpdated.args;
3373
- return [
3374
- {
3375
- ...itemBase(logIndexOfEvent(blacklistUpdated)),
3376
- data: {
3377
- account: lc(a.account),
3378
- policyId: a.policyId.toString(),
3379
- restricted: a.restricted,
3380
- signer,
3381
- updater: lc(a.updater),
3382
- },
3383
- type: 'blacklist-updated',
3726
+ type: 'whitelist-updated',
3727
+ };
3728
+ });
3729
+ recognize(named('BlacklistUpdated'), (event) => {
3730
+ const a = event.args;
3731
+ return {
3732
+ ...itemBase(event.logIndex),
3733
+ data: {
3734
+ account: lc(a.account),
3735
+ policyId: a.policyId.toString(),
3736
+ restricted: a.restricted,
3737
+ signer,
3738
+ updater: lc(a.updater),
3384
3739
  },
3385
- ];
3386
- }
3740
+ type: 'blacklist-updated',
3741
+ };
3742
+ });
3387
3743
  // Address / nonce registry.
3388
- const masterRegistered = findEvent('MasterRegistered');
3389
- if (masterRegistered) {
3390
- const a = masterRegistered.args;
3391
- return [
3392
- {
3393
- ...itemBase(logIndexOfEvent(masterRegistered)),
3394
- data: { masterAddress: lc(a.masterAddress), masterId: hx(a.masterId), signer },
3395
- type: 'master-registered',
3396
- },
3397
- ];
3398
- }
3399
- const nonceIncremented = findEvent('NonceIncremented');
3400
- if (nonceIncremented) {
3401
- const a = nonceIncremented.args;
3402
- return [
3403
- {
3404
- ...itemBase(logIndexOfEvent(nonceIncremented)),
3405
- data: {
3406
- account: lc(a.account),
3407
- nonce: a.newNonce.toString(),
3408
- nonceKey: a.nonceKey.toString(),
3409
- signer,
3410
- },
3411
- type: 'nonce-incremented',
3744
+ recognize(named('MasterRegistered'), (event) => {
3745
+ const a = event.args;
3746
+ return {
3747
+ ...itemBase(event.logIndex),
3748
+ data: { masterAddress: lc(a.masterAddress), masterId: hx(a.masterId), signer },
3749
+ type: 'master-registered',
3750
+ };
3751
+ });
3752
+ recognize(named('NonceIncremented'), (event) => {
3753
+ const a = event.args;
3754
+ return {
3755
+ ...itemBase(event.logIndex),
3756
+ data: {
3757
+ account: lc(a.account),
3758
+ nonce: a.newNonce.toString(),
3759
+ nonceKey: a.nonceKey.toString(),
3760
+ signer,
3412
3761
  },
3413
- ];
3414
- }
3762
+ type: 'nonce-incremented',
3763
+ };
3764
+ });
3415
3765
  // Access-key authorization witnesses.
3416
- const witnessRecorded = findEvent('KeyAuthorizationWitness');
3417
- if (witnessRecorded) {
3418
- const a = witnessRecorded.args;
3419
- return [
3420
- {
3421
- ...itemBase(logIndexOfEvent(witnessRecorded)),
3422
- data: { account: lc(a.account), signer, witness: hx(a.witness) },
3423
- type: 'key-authorization-witness',
3424
- },
3425
- ];
3426
- }
3427
- const witnessBurned = findEvent('KeyAuthorizationWitnessBurned');
3428
- if (witnessBurned) {
3429
- const a = witnessBurned.args;
3430
- return [
3431
- {
3432
- ...itemBase(logIndexOfEvent(witnessBurned)),
3433
- data: { account: lc(a.account), signer, witness: hx(a.witness) },
3434
- type: 'key-authorization-witness-burned',
3435
- },
3436
- ];
3437
- }
3438
- // Validator configuration (rotation supersedes the add/deactivate it bundles).
3439
- const validatorRotated = findEvent('ValidatorRotated');
3440
- if (validatorRotated) {
3441
- const a = validatorRotated.args;
3442
- return [
3443
- {
3444
- ...itemBase(logIndexOfEvent(validatorRotated)),
3445
- data: {
3446
- caller: lc(a.caller),
3447
- deactivatedIndex: a.deactivatedIndex.toString(),
3448
- egress: a.egress,
3449
- index: a.index.toString(),
3450
- ingress: a.ingress,
3451
- newPublicKey: hx(a.newPublicKey),
3452
- oldPublicKey: hx(a.oldPublicKey),
3453
- signer,
3454
- validator: lc(a.validatorAddress),
3455
- },
3456
- type: 'validator-rotated',
3457
- },
3458
- ];
3459
- }
3460
- const validatorAdded = findEvent('ValidatorAdded');
3461
- if (validatorAdded) {
3462
- const a = validatorAdded.args;
3463
- return [
3464
- {
3465
- ...itemBase(logIndexOfEvent(validatorAdded)),
3466
- data: {
3467
- egress: a.egress,
3468
- feeRecipient: lc(a.feeRecipient),
3469
- index: a.index.toString(),
3470
- ingress: a.ingress,
3471
- publicKey: hx(a.publicKey),
3472
- signer,
3473
- validator: lc(a.validatorAddress),
3474
- },
3475
- type: 'validator-added',
3476
- },
3477
- ];
3478
- }
3479
- const validatorDeactivated = findEvent('ValidatorDeactivated');
3480
- if (validatorDeactivated) {
3481
- const a = validatorDeactivated.args;
3482
- return [
3483
- {
3484
- ...itemBase(logIndexOfEvent(validatorDeactivated)),
3485
- data: { index: a.index.toString(), signer, validator: lc(a.validatorAddress) },
3486
- type: 'validator-deactivated',
3487
- },
3488
- ];
3489
- }
3490
- const validatorMigrated = findEvent('ValidatorMigrated');
3491
- if (validatorMigrated) {
3492
- const a = validatorMigrated.args;
3493
- return [
3494
- {
3495
- ...itemBase(logIndexOfEvent(validatorMigrated)),
3496
- data: {
3497
- index: a.index.toString(),
3498
- publicKey: hx(a.publicKey),
3499
- signer,
3500
- validator: lc(a.validatorAddress),
3501
- },
3502
- type: 'validator-migrated',
3766
+ recognize(named('KeyAuthorizationWitness'), (event) => {
3767
+ const a = event.args;
3768
+ return {
3769
+ ...itemBase(event.logIndex),
3770
+ data: { account: lc(a.account), signer, witness: hx(a.witness) },
3771
+ type: 'key-authorization-witness',
3772
+ };
3773
+ });
3774
+ recognize(named('KeyAuthorizationWitnessBurned'), (event) => {
3775
+ const a = event.args;
3776
+ return {
3777
+ ...itemBase(event.logIndex),
3778
+ data: { account: lc(a.account), signer, witness: hx(a.witness) },
3779
+ type: 'key-authorization-witness-burned',
3780
+ };
3781
+ });
3782
+ // Validator configuration. Rotation claims the add/deactivate pair it
3783
+ // bundles, matched by validator index.
3784
+ recognize(named('ValidatorRotated'), (event) => {
3785
+ const a = event.args;
3786
+ unclaimed((e) => {
3787
+ if (!('index' in e.args))
3788
+ return false;
3789
+ const index = e.args.index;
3790
+ return ((e.eventName === 'ValidatorAdded' && index === a.index) ||
3791
+ (e.eventName === 'ValidatorDeactivated' && index === a.deactivatedIndex));
3792
+ }).forEach(claim);
3793
+ return {
3794
+ ...itemBase(event.logIndex),
3795
+ data: {
3796
+ caller: lc(a.caller),
3797
+ deactivatedIndex: a.deactivatedIndex.toString(),
3798
+ egress: a.egress,
3799
+ index: a.index.toString(),
3800
+ ingress: a.ingress,
3801
+ newPublicKey: hx(a.newPublicKey),
3802
+ oldPublicKey: hx(a.oldPublicKey),
3803
+ signer,
3804
+ validator: lc(a.validatorAddress),
3503
3805
  },
3504
- ];
3505
- }
3506
- const validatorMigrationSkipped = findEvent('SkippedValidatorMigration');
3507
- if (validatorMigrationSkipped) {
3508
- const a = validatorMigrationSkipped.args;
3509
- return [
3510
- {
3511
- ...itemBase(logIndexOfEvent(validatorMigrationSkipped)),
3512
- data: {
3513
- index: a.index.toString(),
3514
- publicKey: hx(a.publicKey),
3515
- signer,
3516
- validator: lc(a.validatorAddress),
3517
- },
3518
- type: 'validator-migration-skipped',
3806
+ type: 'validator-rotated',
3807
+ };
3808
+ });
3809
+ recognize(named('ValidatorAdded'), (event) => {
3810
+ const a = event.args;
3811
+ return {
3812
+ ...itemBase(event.logIndex),
3813
+ data: {
3814
+ egress: a.egress,
3815
+ feeRecipient: lc(a.feeRecipient),
3816
+ index: a.index.toString(),
3817
+ ingress: a.ingress,
3818
+ publicKey: hx(a.publicKey),
3819
+ signer,
3820
+ validator: lc(a.validatorAddress),
3519
3821
  },
3520
- ];
3521
- }
3522
- const feeRecipientUpdated = findEvent('FeeRecipientUpdated');
3523
- if (feeRecipientUpdated) {
3524
- const a = feeRecipientUpdated.args;
3525
- return [
3526
- {
3527
- ...itemBase(logIndexOfEvent(feeRecipientUpdated)),
3528
- data: {
3529
- caller: lc(a.caller),
3530
- feeRecipient: lc(a.feeRecipient),
3531
- index: a.index.toString(),
3532
- signer,
3533
- },
3534
- type: 'validator-fee-recipient-set',
3822
+ type: 'validator-added',
3823
+ };
3824
+ });
3825
+ recognize(named('ValidatorDeactivated'), (event) => {
3826
+ const a = event.args;
3827
+ return {
3828
+ ...itemBase(event.logIndex),
3829
+ data: { index: a.index.toString(), signer, validator: lc(a.validatorAddress) },
3830
+ type: 'validator-deactivated',
3831
+ };
3832
+ });
3833
+ recognize(named('ValidatorMigrated'), (event) => {
3834
+ const a = event.args;
3835
+ return {
3836
+ ...itemBase(event.logIndex),
3837
+ data: {
3838
+ index: a.index.toString(),
3839
+ publicKey: hx(a.publicKey),
3840
+ signer,
3841
+ validator: lc(a.validatorAddress),
3535
3842
  },
3536
- ];
3537
- }
3538
- const ipAddressesUpdated = findEvent('IpAddressesUpdated');
3539
- if (ipAddressesUpdated) {
3540
- const a = ipAddressesUpdated.args;
3541
- return [
3542
- {
3543
- ...itemBase(logIndexOfEvent(ipAddressesUpdated)),
3544
- data: {
3545
- caller: lc(a.caller),
3546
- egress: a.egress,
3547
- index: a.index.toString(),
3548
- ingress: a.ingress,
3549
- signer,
3550
- },
3551
- type: 'validator-ip-set',
3843
+ type: 'validator-migrated',
3844
+ };
3845
+ });
3846
+ recognize(named('SkippedValidatorMigration'), (event) => {
3847
+ const a = event.args;
3848
+ return {
3849
+ ...itemBase(event.logIndex),
3850
+ data: {
3851
+ index: a.index.toString(),
3852
+ publicKey: hx(a.publicKey),
3853
+ signer,
3854
+ validator: lc(a.validatorAddress),
3552
3855
  },
3553
- ];
3554
- }
3555
- const validatorOwnershipTransferred = findEvent('ValidatorOwnershipTransferred');
3556
- if (validatorOwnershipTransferred) {
3557
- const a = validatorOwnershipTransferred.args;
3558
- return [
3559
- {
3560
- ...itemBase(logIndexOfEvent(validatorOwnershipTransferred)),
3561
- data: {
3562
- caller: lc(a.caller),
3563
- index: a.index.toString(),
3564
- newAddress: lc(a.newAddress),
3565
- oldAddress: lc(a.oldAddress),
3566
- signer,
3567
- },
3568
- type: 'validator-ownership-transferred',
3856
+ type: 'validator-migration-skipped',
3857
+ };
3858
+ });
3859
+ recognize(named('FeeRecipientUpdated'), (event) => {
3860
+ const a = event.args;
3861
+ return {
3862
+ ...itemBase(event.logIndex),
3863
+ data: {
3864
+ caller: lc(a.caller),
3865
+ feeRecipient: lc(a.feeRecipient),
3866
+ index: a.index.toString(),
3867
+ signer,
3569
3868
  },
3570
- ];
3571
- }
3572
- const ownershipTransferred = findEvent('OwnershipTransferred');
3573
- if (ownershipTransferred) {
3574
- const a = ownershipTransferred.args;
3575
- return [
3576
- {
3577
- ...itemBase(logIndexOfEvent(ownershipTransferred)),
3578
- data: { newOwner: lc(a.newOwner), oldOwner: lc(a.oldOwner), signer },
3579
- type: 'ownership-transferred',
3869
+ type: 'validator-fee-recipient-set',
3870
+ };
3871
+ });
3872
+ recognize(named('IpAddressesUpdated'), (event) => {
3873
+ const a = event.args;
3874
+ return {
3875
+ ...itemBase(event.logIndex),
3876
+ data: {
3877
+ caller: lc(a.caller),
3878
+ egress: a.egress,
3879
+ index: a.index.toString(),
3880
+ ingress: a.ingress,
3881
+ signer,
3580
3882
  },
3581
- ];
3582
- }
3583
- const rotationEpochSet = findEvent('NetworkIdentityRotationEpochSet');
3584
- if (rotationEpochSet) {
3585
- const a = rotationEpochSet.args;
3586
- return [
3587
- {
3588
- ...itemBase(logIndexOfEvent(rotationEpochSet)),
3589
- data: {
3590
- nextEpoch: a.nextEpoch.toString(),
3591
- previousEpoch: a.previousEpoch.toString(),
3592
- signer,
3593
- },
3594
- type: 'network-identity-rotation-epoch-set',
3883
+ type: 'validator-ip-set',
3884
+ };
3885
+ });
3886
+ recognize(named('ValidatorOwnershipTransferred'), (event) => {
3887
+ const a = event.args;
3888
+ return {
3889
+ ...itemBase(event.logIndex),
3890
+ data: {
3891
+ caller: lc(a.caller),
3892
+ index: a.index.toString(),
3893
+ newAddress: lc(a.newAddress),
3894
+ oldAddress: lc(a.oldAddress),
3895
+ signer,
3595
3896
  },
3596
- ];
3597
- }
3598
- const initialized = findEvent('Initialized');
3599
- if (initialized) {
3600
- const a = initialized.args;
3601
- return [
3602
- {
3603
- ...itemBase(logIndexOfEvent(initialized)),
3604
- data: { height: a.height.toString(), signer },
3605
- type: 'initialized',
3897
+ type: 'validator-ownership-transferred',
3898
+ };
3899
+ });
3900
+ recognize(named('OwnershipTransferred'), (event) => {
3901
+ const a = event.args;
3902
+ return {
3903
+ ...itemBase(event.logIndex),
3904
+ data: { newOwner: lc(a.newOwner), oldOwner: lc(a.oldOwner), signer },
3905
+ type: 'ownership-transferred',
3906
+ };
3907
+ });
3908
+ recognize(named('NetworkIdentityRotationEpochSet'), (event) => {
3909
+ const a = event.args;
3910
+ return {
3911
+ ...itemBase(event.logIndex),
3912
+ data: {
3913
+ nextEpoch: a.nextEpoch.toString(),
3914
+ previousEpoch: a.previousEpoch.toString(),
3915
+ signer,
3606
3916
  },
3607
- ];
3608
- }
3609
- // Default: plain transfer (excluding transfers to the fee manager).
3610
- const userTransfers = transfers.filter((e) => !Address.isEqual(e.args.to, Addresses.feeManager));
3611
- if (userTransfers.length > 0) {
3612
- const t = userTransfers[0];
3613
- const args = t.args;
3614
- const to = args.to.toLowerCase();
3615
- const from = args.from.toLowerCase();
3616
- const rawMemo = t.eventName === 'TransferWithMemo' ? args.memo : undefined;
3917
+ type: 'network-identity-rotation-epoch-set',
3918
+ };
3919
+ });
3920
+ recognize(named('Initialized'), (event) => {
3921
+ const a = event.args;
3922
+ return {
3923
+ ...itemBase(event.logIndex),
3924
+ data: { height: a.height.toString(), signer },
3925
+ type: 'initialized',
3926
+ };
3927
+ });
3928
+ // Plain transfers: every remaining viewer-party transfer is its own
3929
+ // activity. Non-viewer transfers are internal legs of what classified
3930
+ // above and surface only via the fallback below.
3931
+ const buildTransfer = (event) => {
3932
+ const args = transferArgs(event);
3933
+ const to = lc(args.to);
3934
+ const from = lc(args.from);
3935
+ const rawMemo = event.eventName === 'TransferWithMemo' ? args.memo : undefined;
3617
3936
  // The raw memo (not the suppressed display string) carries the MPP
3618
3937
  // attribution fingerprint, so resolve the service name from it.
3619
3938
  const memo = rawMemo ? Transfers.memoToString(rawMemo) : undefined;
3620
3939
  const attribution = Mpp.resolve({ fingerprintMap, memo: rawMemo });
3621
- const sourceToken = token(args.amount, t.address);
3940
+ const sourceToken = token(args.amount, event.address);
3622
3941
  if (Address.isEqual(to, viewer))
3623
- return [
3624
- transfer(itemBase(logIndexOfEvent(t)), blockNum, {
3625
- attribution,
3626
- direction: 'in',
3627
- memo,
3628
- recipient: viewer,
3629
- sender: from,
3630
- sourceToken,
3631
- }),
3632
- ...accessKeyItems,
3633
- ];
3634
- return [
3635
- transfer(itemBase(logIndexOfEvent(t)), blockNum, {
3942
+ return transfer(itemBase(event.logIndex), blockNum, {
3636
3943
  attribution,
3637
- direction: 'out',
3944
+ direction: 'in',
3638
3945
  memo,
3639
- recipient: to,
3946
+ recipient: viewer,
3640
3947
  sender: from,
3641
- signer,
3642
3948
  sourceToken,
3643
- }),
3644
- ...accessKeyItems,
3645
- ];
3949
+ });
3950
+ return transfer(itemBase(event.logIndex), blockNum, {
3951
+ attribution,
3952
+ direction: 'out',
3953
+ memo,
3954
+ recipient: to,
3955
+ sender: from,
3956
+ signer,
3957
+ sourceToken,
3958
+ });
3959
+ };
3960
+ const isUserTransfer = (event) => isTransfer(event) && !Address.isEqual(transferArgs(event).to, Addresses.feeManager);
3961
+ recognize((event) => isUserTransfer(event) &&
3962
+ (Address.isEqual(transferArgs(event).from, viewer) ||
3963
+ Address.isEqual(transferArgs(event).to, viewer)), buildTransfer);
3964
+ // Fallback when nothing viewer-facing classified: surface fold-only vault
3965
+ // items, third-party approvals, and transfers (e.g. a sponsored batch)
3966
+ // instead of reporting `unknown`.
3967
+ if (items.length === 0) {
3968
+ items.push(...suppressedVaultItems);
3969
+ recognize(isErc20Approval, approvalItem);
3970
+ recognize(isUserTransfer, buildTransfer);
3646
3971
  }
3647
- // Pure access-key tx (no transfer / mint / burn / etc.).
3648
- if (accessKeyItems.length > 0)
3649
- return accessKeyItems;
3650
- // Unclassified: a tx the address sent or paid for whose logs matched no known
3651
- // classifier. Surface it honestly as an `unknown` item carrying every log
3652
- // (decoded when possible), rather than fabricating a zero transfer.
3653
- return [unknown(itemBase(minLogIndex), { events: buildTxEvents(rows, events), signer })];
3654
- }
3655
- /** Extracts the block-wide log index from a decoded viem event. */
3656
- function logIndexOfEvent(event) {
3657
- return event.logIndex;
3972
+ // Unclassified: a tx the address sent or paid for whose logs matched no
3973
+ // known classifier. Surface it honestly as an `unknown` item carrying every
3974
+ // log (decoded when possible), rather than fabricating a zero transfer.
3975
+ if (items.length === 0)
3976
+ return [unknown(itemBase(minLogIndex), { events: buildTxEvents(rows, events), signer })];
3977
+ return items.sort((a, b) => a.logIndex - b.logIndex);
3658
3978
  }
3659
3979
  /**
3660
3980
  * Builds a `transfer` item whose `data` is the `Transfer` API resource plus the
@@ -3693,29 +4013,31 @@ function unknown(base, data) {
3693
4013
  }
3694
4014
  /** Decodes a group's raw rows into `viem/tempo` events via `parseEventLogs` (non-strict). */
3695
4015
  function decodeRows(rows) {
3696
- return rows.flatMap((r) => {
3697
- const log = {
3698
- address: r.logAddress,
3699
- blockHash: '0x0',
3700
- blockNumber: BigInt(r.blockNum),
3701
- data: (r.data ?? '0x'),
3702
- logIndex: r.logIdx,
3703
- removed: false,
3704
- topics: [r.selector, r.topic1, r.topic2, r.topic3].filter(Boolean),
3705
- transactionHash: r.txHash,
3706
- transactionIndex: r.txIdx,
3707
- };
3708
- try {
3709
- return parseEventLogs({
3710
- abi: [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault],
3711
- logs: [log],
3712
- strict: false,
3713
- });
3714
- }
3715
- catch {
3716
- return [];
3717
- }
3718
- });
4016
+ const logs = rows.map((r) => ({
4017
+ address: r.logAddress,
4018
+ blockHash: '0x0',
4019
+ blockNumber: BigInt(r.blockNum),
4020
+ data: (r.data ?? '0x'),
4021
+ logIndex: r.logIdx,
4022
+ removed: false,
4023
+ topics: [r.selector, r.topic1, r.topic2, r.topic3].filter(Boolean),
4024
+ transactionHash: r.txHash,
4025
+ transactionIndex: r.txIdx,
4026
+ }));
4027
+ try {
4028
+ // One batched call: viem re-processes the merged ABI per invocation, so
4029
+ // per-row calls cost ~55x more. Non-strict decoding skips undecodable
4030
+ // logs per log and can yield `args: undefined`; normalize so `in`-guards
4031
+ // never throw.
4032
+ return parseEventLogs({
4033
+ abi: [...Abis.abis, ...Abis.earnRouter, ...Abis.earnVault, ...erc4626Events],
4034
+ logs,
4035
+ strict: false,
4036
+ }).map((event) => ({ ...event, args: event.args ?? {} }));
4037
+ }
4038
+ catch {
4039
+ return [];
4040
+ }
3719
4041
  }
3720
4042
  /**
3721
4043
  * Builds the full per-transaction `events` capture: one entry per raw log,
@@ -3873,8 +4195,8 @@ async function enrichTokens(c, options) {
3873
4195
  rates,
3874
4196
  };
3875
4197
  }
3876
- /** Resolves token metadata on each item's source Zone without changing feed order. */
3877
- async function enrichZoneTokens(c, options) {
4198
+ /** Resolves token metadata on each item's source chain without changing feed order. */
4199
+ async function enrichChainTokens(c, options) {
3878
4200
  const byChain = new Map();
3879
4201
  for (const item of options.items) {
3880
4202
  const items = byChain.get(item.chainId) ?? [];