tapimo 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/dist/ApiKey.d.ts +2 -1
  2. package/dist/ApiKey.d.ts.map +1 -1
  3. package/dist/ApiKeys.d.ts +4 -2
  4. package/dist/ApiKeys.d.ts.map +1 -1
  5. package/dist/ApiKeys.js +26 -1
  6. package/dist/ApiKeys.js.map +1 -1
  7. package/dist/App.d.ts +6007 -5003
  8. package/dist/App.d.ts.map +1 -1
  9. package/dist/App.js +1 -0
  10. package/dist/App.js.map +1 -1
  11. package/dist/Webhooks.d.ts +1 -1
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +6 -6
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +91 -84
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +22 -13
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +6 -0
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +4 -2
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +69 -69
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts.map +1 -1
  27. package/dist/analytics/Analytics.js +232 -0
  28. package/dist/analytics/Analytics.js.map +1 -1
  29. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  30. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  31. package/dist/analytics/tables/requestEvents.js +2 -0
  32. package/dist/analytics/tables/requestEvents.js.map +1 -1
  33. package/dist/apps/Zones.d.ts +4266 -82
  34. package/dist/apps/Zones.d.ts.map +1 -1
  35. package/dist/apps/Zones.js +27 -8
  36. package/dist/apps/Zones.js.map +1 -1
  37. package/dist/apps/data/App.d.ts +3467 -2696
  38. package/dist/apps/data/App.d.ts.map +1 -1
  39. package/dist/apps/data/App.js +2 -0
  40. package/dist/apps/data/App.js.map +1 -1
  41. package/dist/apps/data/routes/activities.d.ts +2999 -100
  42. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  43. package/dist/apps/data/routes/activities.js +340 -2
  44. package/dist/apps/data/routes/activities.js.map +1 -1
  45. package/dist/apps/data/routes/addresses.d.ts +514 -0
  46. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  47. package/dist/apps/data/routes/balances.d.ts +26 -26
  48. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  49. package/dist/apps/data/routes/balances.js +7 -3
  50. package/dist/apps/data/routes/balances.js.map +1 -1
  51. package/dist/apps/data/routes/blocks.d.ts +4 -4
  52. package/dist/apps/data/routes/earn.d.ts +75 -75
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +65 -36
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts +124 -120
  57. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  58. package/dist/apps/data/routes/exchanges.js +115 -77
  59. package/dist/apps/data/routes/exchanges.js.map +1 -1
  60. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  61. package/dist/apps/data/routes/indexer.d.ts +2 -1
  62. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  63. package/dist/apps/data/routes/indexer.js +3 -2
  64. package/dist/apps/data/routes/indexer.js.map +1 -1
  65. package/dist/apps/data/routes/receipts.d.ts +4 -3
  66. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  67. package/dist/apps/data/routes/receipts.js +15 -14
  68. package/dist/apps/data/routes/receipts.js.map +1 -1
  69. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  70. package/dist/apps/data/routes/rpc.js +5 -1
  71. package/dist/apps/data/routes/rpc.js.map +1 -1
  72. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  73. package/dist/apps/data/routes/tokens.d.ts +1 -1
  74. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  75. package/dist/apps/data/routes/tokens.js +4 -3
  76. package/dist/apps/data/routes/tokens.js.map +1 -1
  77. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  78. package/dist/apps/data/routes/transfers.js +25 -7
  79. package/dist/apps/data/routes/transfers.js.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  81. package/dist/apps/data/routes/webhooks.d.ts +71 -1
  82. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/data/routes/webhooks.js +177 -23
  84. package/dist/apps/data/routes/webhooks.js.map +1 -1
  85. package/dist/apps/data/routes/zones.js +1 -1
  86. package/dist/apps/data/routes/zones.js.map +1 -1
  87. package/dist/apps/funding/App.d.ts +322 -89
  88. package/dist/apps/funding/App.d.ts.map +1 -1
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts +375 -45
  90. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  91. package/dist/apps/funding/routes/deposit-addresses.js +208 -12
  92. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  93. package/dist/apps/funding/routes/transfers.d.ts +32 -32
  94. package/dist/apps/funding/routes/transfers.js +1 -1
  95. package/dist/apps/funding/routes/transfers.js.map +1 -1
  96. package/dist/apps/management/App.d.ts +418 -418
  97. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  98. package/dist/apps/management/routes/billing.d.ts +72 -72
  99. package/dist/apps/management/routes/billing.js +1 -1
  100. package/dist/apps/management/routes/billing.js.map +1 -1
  101. package/dist/apps/management/routes/faucet.d.ts +8 -8
  102. package/dist/apps/management/routes/faucet.js +2 -2
  103. package/dist/apps/management/routes/faucet.js.map +1 -1
  104. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  105. package/dist/apps/management/routes/me.d.ts +6 -6
  106. package/dist/apps/management/routes/members.d.ts +14 -14
  107. package/dist/apps/management/routes/projects.d.ts +40 -40
  108. package/dist/apps/management/routes/usage.d.ts +8 -8
  109. package/dist/apps/management/routes/webhooks.d.ts +49 -49
  110. package/dist/apps/mcp.d.ts.map +1 -1
  111. package/dist/apps/mcp.js +4 -1
  112. package/dist/apps/mcp.js.map +1 -1
  113. package/dist/db/Db.d.ts +2 -0
  114. package/dist/db/Db.d.ts.map +1 -1
  115. package/dist/db/Db.js +90 -0
  116. package/dist/db/Db.js.map +1 -1
  117. package/dist/db/Schema.d.ts +19 -0
  118. package/dist/db/Schema.d.ts.map +1 -1
  119. package/dist/db/Schema.js +24 -0
  120. package/dist/db/Schema.js.map +1 -1
  121. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  122. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  123. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  124. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  125. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  126. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  127. package/dist/db/tables/fundingDeposits.js +69 -0
  128. package/dist/db/tables/fundingDeposits.js.map +1 -1
  129. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  130. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  131. package/dist/db/tables/webhookQueueEvents.js +160 -0
  132. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  133. package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
  134. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  135. package/dist/db/tables/webhookSubscriptions.js +10 -0
  136. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  137. package/dist/internal/Auth.d.ts +3 -26
  138. package/dist/internal/Auth.d.ts.map +1 -1
  139. package/dist/internal/Auth.js +6 -37
  140. package/dist/internal/Auth.js.map +1 -1
  141. package/dist/internal/Credential.d.ts +27 -0
  142. package/dist/internal/Credential.d.ts.map +1 -0
  143. package/dist/internal/Credential.js +34 -0
  144. package/dist/internal/Credential.js.map +1 -0
  145. package/dist/internal/EarnRates.js +11 -7
  146. package/dist/internal/EarnRates.js.map +1 -1
  147. package/dist/internal/Log.d.ts +8 -0
  148. package/dist/internal/Log.d.ts.map +1 -1
  149. package/dist/internal/Log.js +4 -0
  150. package/dist/internal/Log.js.map +1 -1
  151. package/dist/internal/MetricSink.d.ts +51 -0
  152. package/dist/internal/MetricSink.d.ts.map +1 -1
  153. package/dist/internal/MetricSink.js +54 -2
  154. package/dist/internal/MetricSink.js.map +1 -1
  155. package/dist/internal/RateLimit.d.ts +4 -6
  156. package/dist/internal/RateLimit.d.ts.map +1 -1
  157. package/dist/internal/RateLimit.js +15 -14
  158. package/dist/internal/RateLimit.js.map +1 -1
  159. package/dist/internal/Store.d.ts +2 -1
  160. package/dist/internal/Store.d.ts.map +1 -1
  161. package/dist/internal/Store.js +57 -18
  162. package/dist/internal/Store.js.map +1 -1
  163. package/dist/internal/Tidx.d.ts +4 -3
  164. package/dist/internal/Tidx.d.ts.map +1 -1
  165. package/dist/internal/Tidx.js +2 -2
  166. package/dist/internal/Tidx.js.map +1 -1
  167. package/dist/internal/Value.d.ts +2 -0
  168. package/dist/internal/Value.d.ts.map +1 -1
  169. package/dist/internal/Value.js +25 -0
  170. package/dist/internal/Value.js.map +1 -1
  171. package/dist/internal/Viem.d.ts +4 -3
  172. package/dist/internal/Viem.d.ts.map +1 -1
  173. package/dist/internal/Viem.js +3 -2
  174. package/dist/internal/Viem.js.map +1 -1
  175. package/dist/internal/WebhookTransfer.d.ts +20 -0
  176. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  177. package/dist/internal/WebhookTransfer.js +7 -4
  178. package/dist/internal/WebhookTransfer.js.map +1 -1
  179. package/dist/internal/Webhooks.d.ts +141 -0
  180. package/dist/internal/Webhooks.d.ts.map +1 -1
  181. package/dist/internal/Webhooks.js +139 -10
  182. package/dist/internal/Webhooks.js.map +1 -1
  183. package/dist/internal/funding/providers/relay.d.ts +3 -4
  184. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  185. package/dist/internal/funding/providers/relay.js +59 -8
  186. package/dist/internal/funding/providers/relay.js.map +1 -1
  187. package/dist/internal/index.d.ts +1 -0
  188. package/dist/internal/index.d.ts.map +1 -1
  189. package/dist/internal/index.js +1 -0
  190. package/dist/internal/index.js.map +1 -1
  191. package/package.json +3 -3
  192. package/src/ApiKeys.test.ts +20 -0
  193. package/src/ApiKeys.ts +28 -1
  194. package/src/App.test.ts +40 -8
  195. package/src/App.ts +1 -0
  196. package/src/Client.test-d.ts +34 -1
  197. package/src/Webhooks.ts +20 -18
  198. package/src/admin/apps/api-keys.test.ts +9 -0
  199. package/src/admin/apps/api-keys.ts +15 -0
  200. package/src/admin/ui/src/lib/query-client.ts +3 -2
  201. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  202. package/src/admin/ui.generated.ts +69 -69
  203. package/src/analytics/Analytics.test.ts +42 -0
  204. package/src/analytics/Analytics.ts +232 -0
  205. package/src/analytics/tables/requestEvents.ts +6 -0
  206. package/src/apps/Zones.test.ts +20 -3
  207. package/src/apps/Zones.ts +28 -8
  208. package/src/apps/composition.test.ts +2 -0
  209. package/src/apps/data/App.test.ts +2 -2
  210. package/src/apps/data/App.ts +2 -0
  211. package/src/apps/data/routes/activities.bench.ts +41 -0
  212. package/src/apps/data/routes/activities.test.ts +103 -2
  213. package/src/apps/data/routes/activities.ts +401 -2
  214. package/src/apps/data/routes/balances.bench.ts +21 -0
  215. package/src/apps/data/routes/balances.ts +7 -3
  216. package/src/apps/data/routes/blocks.bench.ts +34 -0
  217. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  218. package/src/apps/data/routes/earn.bench.ts +71 -0
  219. package/src/apps/data/routes/earn.ts +66 -36
  220. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  221. package/src/apps/data/routes/exchanges.test.ts +8 -3
  222. package/src/apps/data/routes/exchanges.ts +134 -82
  223. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  224. package/src/apps/data/routes/indexer.bench.ts +8 -0
  225. package/src/apps/data/routes/indexer.test.ts +2 -1
  226. package/src/apps/data/routes/indexer.ts +3 -2
  227. package/src/apps/data/routes/receipts.bench.ts +39 -0
  228. package/src/apps/data/routes/receipts.ts +16 -14
  229. package/src/apps/data/routes/rpc.bench.ts +14 -0
  230. package/src/apps/data/routes/rpc.test.ts +27 -0
  231. package/src/apps/data/routes/rpc.ts +5 -1
  232. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  233. package/src/apps/data/routes/tokens.bench.ts +76 -0
  234. package/src/apps/data/routes/tokens.ts +3 -2
  235. package/src/apps/data/routes/transactions.bench.ts +48 -0
  236. package/src/apps/data/routes/transfers.bench.ts +28 -0
  237. package/src/apps/data/routes/transfers.ts +36 -10
  238. package/src/apps/data/routes/valuation.bench.ts +14 -0
  239. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  240. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  241. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  242. package/src/apps/data/routes/webhooks.test.ts +53 -6
  243. package/src/apps/data/routes/webhooks.ts +252 -24
  244. package/src/apps/data/routes/zones.bench.ts +32 -0
  245. package/src/apps/data/routes/zones.test.ts +1 -1
  246. package/src/apps/data/routes/zones.ts +1 -1
  247. package/src/apps/funding/routes/chains.test.ts +1 -1
  248. package/src/apps/funding/routes/deposit-addresses.test.ts +217 -3
  249. package/src/apps/funding/routes/deposit-addresses.ts +255 -19
  250. package/src/apps/funding/routes/providers.test.ts +1 -1
  251. package/src/apps/funding/routes/quotes.test.ts +1 -1
  252. package/src/apps/funding/routes/transfers.test.ts +20 -8
  253. package/src/apps/funding/routes/transfers.ts +1 -1
  254. package/src/apps/management/routes/billing.ts +1 -1
  255. package/src/apps/management/routes/faucet.ts +2 -2
  256. package/src/apps/management/routes/me.test.ts +31 -0
  257. package/src/apps/mcp.ts +4 -1
  258. package/src/db/Db.ts +117 -0
  259. package/src/db/Schema.ts +32 -0
  260. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  261. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  262. package/src/db/tables/fundingDeposits.test.ts +82 -0
  263. package/src/db/tables/fundingDeposits.ts +130 -0
  264. package/src/db/tables/webhookQueueEvents.ts +285 -0
  265. package/src/db/tables/webhookSubscriptions.ts +15 -0
  266. package/src/internal/Auth.test.ts +61 -98
  267. package/src/internal/Auth.ts +9 -63
  268. package/src/internal/Credential.test.ts +51 -0
  269. package/src/internal/Credential.ts +58 -0
  270. package/src/internal/EarnRates.ts +13 -7
  271. package/src/internal/EarnVaults.test.ts +2 -2
  272. package/src/internal/Log.test.ts +3 -1
  273. package/src/internal/Log.ts +14 -0
  274. package/src/internal/MetricSink.test.ts +108 -2
  275. package/src/internal/MetricSink.ts +112 -2
  276. package/src/internal/RateLimit.test.ts +23 -18
  277. package/src/internal/RateLimit.ts +23 -15
  278. package/src/internal/Store.test.ts +99 -13
  279. package/src/internal/Store.ts +52 -17
  280. package/src/internal/Tidx.test.ts +4 -4
  281. package/src/internal/Tidx.ts +5 -4
  282. package/src/internal/Value.test.ts +25 -0
  283. package/src/internal/Value.ts +29 -0
  284. package/src/internal/Viem.test.ts +4 -0
  285. package/src/internal/Viem.ts +6 -4
  286. package/src/internal/WebhookTransfer.test.ts +89 -0
  287. package/src/internal/WebhookTransfer.ts +11 -7
  288. package/src/internal/Webhooks.test.ts +488 -0
  289. package/src/internal/Webhooks.ts +291 -10
  290. package/src/internal/funding/Provider.test.ts +1 -1
  291. package/src/internal/funding/providers/index.test.ts +73 -3
  292. package/src/internal/funding/providers/relay.test.ts +1 -5
  293. package/src/internal/funding/providers/relay.ts +56 -12
  294. package/src/internal/index.test.ts +1 -0
  295. package/src/internal/index.ts +1 -0
@@ -1,4 +1,4 @@
1
- import { type Db, Webhooks as Core } from 'tapimo'
1
+ import { type Db, Store, Webhooks as Core } from 'tapimo'
2
2
  import * as z from 'zod/mini'
3
3
 
4
4
  import * as Cursor from '../../../internal/Cursor.js'
@@ -1522,7 +1522,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1522
1522
  "logIndex": 0,
1523
1523
  "recipient": "0x9e39034aae71fb89f66061a2602eb6efec271754",
1524
1524
  "sender": "0xe7687128b0a808c2831ff94d4f7b2fb35c65af38",
1525
- "timestamp": "2026-01-14T18:38:03.000Z",
1525
+ "timestamp": "2026-01-14T18:38:03.685Z",
1526
1526
  "token": {
1527
1527
  "decimals": 6,
1528
1528
  "symbol": "QKSUSD",
@@ -1540,6 +1540,52 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1540
1540
  `)
1541
1541
  })
1542
1542
 
1543
+ test('delivers collision-shaped token metadata from a cache hit', async () => {
1544
+ const db = TestApp.database()
1545
+ const subscription = await createTransferSubscription(db)
1546
+ await Core.setCursor(db, subscription.id, windowCursor)
1547
+ const cache = Store.memory()
1548
+ const address = '0x20c00000000000000000000071de0cd31ab0d105'
1549
+ await Store.memoize(
1550
+ async () => ({
1551
+ currency: 'USD',
1552
+ decimals: 6,
1553
+ name: 'Collision token',
1554
+ symbol: '123#__bigint',
1555
+ }),
1556
+ {
1557
+ key: `webhook:token-meta:v2:42431:${address}`,
1558
+ store: cache,
1559
+ ttl: 60_000,
1560
+ },
1561
+ )
1562
+ const bodies: string[] = []
1563
+ const fetch = (async (_input, init) => {
1564
+ bodies.push(typeof init?.body === 'string' ? init.body : '')
1565
+ return new Response(null, { status: 200 })
1566
+ }) satisfies typeof globalThis.fetch
1567
+
1568
+ const result = await Webhooks.poll({
1569
+ cache,
1570
+ chainId: 42431,
1571
+ client,
1572
+ db,
1573
+ dispatch: ({ envelope, subscription }) =>
1574
+ Core.deliverAndRecord(db, subscription, envelope, { fetch }),
1575
+ eventTypes: ['token:transfer'],
1576
+ headBlockNumber: windowHead,
1577
+ })
1578
+
1579
+ const payloads = bodies.map((body) => Webhooks.schema.Envelope.parse(JSON.parse(body)))
1580
+ const delivered = payloads.find(
1581
+ (payload) => payload.type === 'token:transfer' && payload.data.address === address,
1582
+ )
1583
+ expect(result.dispatched).toBe(2)
1584
+ expect(delivered?.type === 'token:transfer' ? delivered.data.token?.symbol : undefined).toBe(
1585
+ '123#__bigint',
1586
+ )
1587
+ })
1588
+
1543
1589
  test('scans event types concurrently without increasing scan bounds', async () => {
1544
1590
  const db = TestApp.database()
1545
1591
  const transfer = await createTransferSubscription(db)
@@ -1605,7 +1651,8 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1605
1651
  chainId: 42431,
1606
1652
  client,
1607
1653
  db,
1608
- dispatchBatch: async (dispatchables) => {
1654
+ dispatchBatch: async (dispatchables, context) => {
1655
+ expect(context.db).toBe(db)
1609
1656
  batches.push([...dispatchables])
1610
1657
  return { ok: true }
1611
1658
  },
@@ -2070,7 +2117,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2070
2117
  "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2071
2118
  },
2072
2119
  "logIndex": 0,
2073
- "timestamp": "2026-01-14T18:38:03.000Z",
2120
+ "timestamp": "2026-01-14T18:38:03.685Z",
2074
2121
  "topics": [
2075
2122
  "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2076
2123
  "0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
@@ -2113,7 +2160,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2113
2160
  "blockNumber": 1000002,
2114
2161
  "data": "0x0000000000000000000000000000000000000000000000000000000000002710",
2115
2162
  "logIndex": 0,
2116
- "timestamp": "2026-01-14T18:38:03.000Z",
2163
+ "timestamp": "2026-01-14T18:38:03.685Z",
2117
2164
  "topics": [
2118
2165
  "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
2119
2166
  "0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
@@ -2290,7 +2337,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2290
2337
  "miner": "0x0000000000000000000000000000000000000000",
2291
2338
  "number": 1000002,
2292
2339
  "parentHash": "0xa406dff9c51cc2abbef3a811f0499e99a568bf781019f129ab45dca1df0e43e5",
2293
- "timestamp": "2026-01-14T18:38:03.000Z",
2340
+ "timestamp": "2026-01-14T18:38:03.685Z",
2294
2341
  "transactionCount": 52,
2295
2342
  }
2296
2343
  `)
@@ -664,7 +664,7 @@ export namespace schema {
664
664
  .datetime()
665
665
  .check(
666
666
  z.describe('Block timestamp as an ISO 8601 string.'),
667
- z.meta({ examples: ['2026-01-14T18:38:03.000Z'] }),
667
+ z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
668
668
  ),
669
669
  token: z.optional(
670
670
  z
@@ -790,7 +790,7 @@ export namespace schema {
790
790
  .datetime()
791
791
  .check(
792
792
  z.describe('When the block was produced, as an ISO 8601 timestamp.'),
793
- z.meta({ examples: ['2026-01-14T18:38:03.000Z'] }),
793
+ z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
794
794
  ),
795
795
  topics: z.array(Schema.Hash).check(
796
796
  z.describe('Log topics, starting with the event signature hash (topic 0).'),
@@ -881,7 +881,7 @@ export namespace schema {
881
881
  .datetime()
882
882
  .check(
883
883
  z.describe('When the block was produced, as an ISO 8601 timestamp.'),
884
- z.meta({ examples: ['2026-01-14T18:38:03.000Z'] }),
884
+ z.meta({ examples: ['2026-01-14T18:38:03.685Z'] }),
885
885
  ),
886
886
  })
887
887
  .check(z.describe('Payload for a `block:created` event (a new block header).'))
@@ -2276,16 +2276,11 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
2276
2276
  // Token metadata is best-effort for webhooks: transfers use it for display,
2277
2277
  // while transactions expose fee-token references without blocking delivery.
2278
2278
  const enrichStartedAt = Date.now()
2279
- const data =
2280
- eventType === 'token:transfer'
2281
- ? await withTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
2282
- : eventType === 'transaction:included'
2283
- ? await withFeeTokenMetadata(item.data, {
2284
- cache: cache_metadata,
2285
- chainId,
2286
- client,
2287
- })
2288
- : item.data
2279
+ const data = await enrichEventData(eventType, item.data, {
2280
+ cache: cache_metadata,
2281
+ chainId,
2282
+ client,
2283
+ })
2289
2284
  durations.enrich += Date.now() - enrichStartedAt
2290
2285
  observed.add('enrich')
2291
2286
  options.signal?.throwIfAborted()
@@ -2303,7 +2298,7 @@ export async function poll(options: poll.Options): Promise<poll.Result> {
2303
2298
  const dispatchables: poll.Dispatchable[] = []
2304
2299
  for (const item of page.items) dispatchables.push(await buildDispatchable(item))
2305
2300
  options.signal?.throwIfAborted()
2306
- const delivered = await dispatchBatch(dispatchables)
2301
+ const delivered = await dispatchBatch(dispatchables, { db })
2307
2302
  if (delivered.ok) {
2308
2303
  accepted = page.items.at(-1)?.cursor
2309
2304
  result.dispatched += dispatchables.length
@@ -2406,7 +2401,9 @@ export declare namespace poll {
2406
2401
  */
2407
2402
  dispatch?: ((input: Dispatchable) => Promise<Webhooks.Result>) | undefined
2408
2403
  /** Batch delivery transport. A successful result confirms every row in the input. */
2409
- dispatchBatch?: ((input: readonly Dispatchable[]) => Promise<Webhooks.Result>) | undefined
2404
+ dispatchBatch?:
2405
+ | ((input: readonly Dispatchable[], context: DispatchContext) => Promise<Webhooks.Result>)
2406
+ | undefined
2410
2407
  /** Event types to poll (default all). */
2411
2408
  eventTypes?: readonly EventType[] | undefined
2412
2409
  /** Current chain head; the poller emits up to `head - confirmations`. */
@@ -2426,6 +2423,12 @@ export declare namespace poll {
2426
2423
  /** Stops scanning and dispatch when the caller loses its execution lease. */
2427
2424
  signal?: AbortSignal | undefined
2428
2425
  }
2426
+
2427
+ /** Runtime context shared with a batch delivery transport. */
2428
+ type DispatchContext = {
2429
+ /** Database instance already resolved for this poll. */
2430
+ db: Db.Db
2431
+ }
2429
2432
  /** Summary counters for one poll tick. */
2430
2433
  type Result = {
2431
2434
  /** Chain that was polled. */
@@ -3066,6 +3069,32 @@ function decodeScanLog(log: ScanLog, event: AbiEvent): ScanLog {
3066
3069
  }
3067
3070
  }
3068
3071
 
3072
+ /**
3073
+ * Attaches best-effort token metadata to one matched row before delivery.
3074
+ * Transfers gain display metadata; transactions resolve their fee token.
3075
+ */
3076
+ export async function enrichEventData(
3077
+ eventType: EventType,
3078
+ data: unknown,
3079
+ options: enrichEventData.Options,
3080
+ ): Promise<unknown> {
3081
+ if (eventType === 'token:transfer') return withTokenMetadata(data, options)
3082
+ if (eventType === 'transaction:included') return withFeeTokenMetadata(data, options)
3083
+ return data
3084
+ }
3085
+
3086
+ export declare namespace enrichEventData {
3087
+ /** Metadata lookup dependencies. */
3088
+ type Options = {
3089
+ /** Memoization store for token metadata (5 minute TTL). */
3090
+ cache: Store.Store
3091
+ /** Chain the row was produced on. */
3092
+ chainId: number
3093
+ /** Client for `token.getMetadata` reads. */
3094
+ client: Viem.getClient.ReturnType
3095
+ }
3096
+ }
3097
+
3069
3098
  /** Scans contract logs for one subscription, then re-checks every filter in memory. */
3070
3099
  async function scanLogs(options: {
3071
3100
  batchSize: number
@@ -3124,7 +3153,12 @@ async function scanLogs(options: {
3124
3153
  if (blockNumber === undefined || logIndex === undefined) continue
3125
3154
  if (atOrBefore(blockNumber, logIndex, window.skip)) continue
3126
3155
  if (!matchesLog(log, f, event)) continue
3127
- const data = toLogEvent(log, blockIso(await cache.header(blockNumber)), f.signature, event)
3156
+ const data = toLogEvent(
3157
+ log,
3158
+ Value.blockToIso(await cache.header(blockNumber)),
3159
+ f.signature,
3160
+ event,
3161
+ )
3128
3162
  if (data) {
3129
3163
  items.push({ cursor: [data.blockNumber, data.logIndex], data })
3130
3164
  if (items.length >= batchSize) return { hasMore: true, items }
@@ -3178,12 +3212,6 @@ function matchesLog(
3178
3212
 
3179
3213
  // block:created (getBlock)
3180
3214
 
3181
- /** Converts a viem block's `timestamp` (seconds) to an ISO 8601 string. */
3182
- function blockIso(block: RpcBlock | undefined): string | undefined {
3183
- if (!block || typeof block.timestamp !== 'bigint') return undefined
3184
- return new Date(Number(block.timestamp) * 1000).toISOString()
3185
- }
3186
-
3187
3215
  type BlockEventData = {
3188
3216
  number: number
3189
3217
  hash: string
@@ -3203,7 +3231,7 @@ function toBlockEvent(block: RpcBlock): BlockEventData | undefined {
3203
3231
  const miner = Schema.Address.safeParse(block.miner)
3204
3232
  const gasUsed = typeof block.gasUsed === 'bigint' ? Number(block.gasUsed) : undefined
3205
3233
  const gasLimit = typeof block.gasLimit === 'bigint' ? Number(block.gasLimit) : undefined
3206
- const timestamp = blockIso(block)
3234
+ const timestamp = Value.blockToIso(block)
3207
3235
  const transactionCount = Array.isArray(block.transactions) ? block.transactions.length : 0
3208
3236
  if (
3209
3237
  number === undefined ||
@@ -3318,6 +3346,9 @@ async function scanTxs(options: {
3318
3346
  ? (block['transactions'] as unknown[])
3319
3347
  : []
3320
3348
  const timestamp = asString(block['timestamp'])
3349
+ // A transaction's own `blockTimestamp` is seconds-only, so the delivered
3350
+ // timestamp comes from the enclosing block, which carries milliseconds.
3351
+ const blockTime = Value.blockToIso(block)
3321
3352
  for (let offset = 0; offset < transactions.length; offset++) {
3322
3353
  const transaction = transactions[offset] as Record<string, unknown>
3323
3354
  const transactionIndex = Value.hexToNumber(transaction['transactionIndex']) ?? offset
@@ -3325,7 +3356,8 @@ async function scanTxs(options: {
3325
3356
  if (!matchesTx(transaction, f, includeCalls, timestamp, blockNumber)) continue
3326
3357
  const parsed = Transactions.schema.Rpc.Transaction.safeParse(transaction)
3327
3358
  if (!parsed.success) continue
3328
- const data = Transactions.humanizeTransaction(parsed.data)
3359
+ const humanized = Transactions.humanizeTransaction(parsed.data)
3360
+ const data = blockTime === undefined ? humanized : { ...humanized, timestamp: blockTime }
3329
3361
  items.push({ cursor: [blockNumber, transactionIndex], data })
3330
3362
  if (items.length >= batchSize) return { hasMore: true, items }
3331
3363
  }
@@ -3482,6 +3514,202 @@ function unauthorized(c: Context<App.Environment>) {
3482
3514
  })
3483
3515
  }
3484
3516
 
3517
+ // Pure per-block scanning (no RPC, no cursors, no pagination).
3518
+
3519
+ // Pure over its inputs: the caller fetches the raw block (with transactions)
3520
+ // and its logs; enrichment happens downstream. Rows require a parsable block number.
3521
+ /**
3522
+ * Matches one fetched block against a subscription set, producing the rows
3523
+ * each event type delivers. Invalid stored filters fail closed into `invalid`.
3524
+ */
3525
+ export function scanBlock<const subscription extends scanBlock.Subscription>(
3526
+ options: scanBlock.Options<subscription>,
3527
+ ): scanBlock.Result<subscription> {
3528
+ const { block, logs, subscriptions } = options
3529
+ const blockNumber = Value.hexToNumber(block['number'])
3530
+ const timestamp = Value.blockToIso(block)
3531
+ const header = toScanHeader(block)
3532
+ const viemLogs = logs.map(fromRpcLog)
3533
+ const matches: scanBlock.Match<subscription>[] = []
3534
+ const invalid: scanBlock.Invalid<subscription>[] = []
3535
+
3536
+ for (const subscription of subscriptions) {
3537
+ const filters = (() => {
3538
+ try {
3539
+ return parseFilters(subscription.eventType, subscription.filters)
3540
+ } catch (error) {
3541
+ invalid.push({ error, subscription })
3542
+ return undefined
3543
+ }
3544
+ })()
3545
+ if (filters === undefined) continue
3546
+ const matched = matches.length
3547
+ const push = (data: unknown, logIndex: number) => {
3548
+ if (blockNumber !== undefined) matches.push({ blockNumber, data, logIndex, subscription })
3549
+ }
3550
+
3551
+ // A throwing matcher invalidates only this subscription, rolling back any
3552
+ // rows it matched before the failure.
3553
+ try {
3554
+ switch (subscription.eventType) {
3555
+ case 'block:created': {
3556
+ const f = filters as z.output<typeof schema.BlockFilters>
3557
+ if (!matchesBlock(header, f)) break
3558
+ const data = toBlockEvent(header)
3559
+ if (data) push(data, 0)
3560
+ break
3561
+ }
3562
+ case 'log:emitted': {
3563
+ const f = filters as z.output<typeof schema.LogFilters>
3564
+ const event = parseEvent(f.signature)
3565
+ if (f.signature && (!event || !/^[A-Za-z_]\w*$/.test(event.name))) {
3566
+ invalid.push({
3567
+ error: new Webhooks.InvalidFilterError('log:emitted', [
3568
+ { message: 'Unsupported event signature.', path: ['signature'] },
3569
+ ]),
3570
+ subscription,
3571
+ })
3572
+ break
3573
+ }
3574
+ // Validate arg names up front so a bad filter fails closed even
3575
+ // when the block has no decodable logs.
3576
+ const unknown =
3577
+ event && f.args
3578
+ ? Object.keys(f.args).find(
3579
+ (name) => !event.inputs.some((input) => input.name === name),
3580
+ )
3581
+ : undefined
3582
+ if (unknown !== undefined) {
3583
+ invalid.push({
3584
+ error: new Webhooks.InvalidFilterError('log:emitted', [
3585
+ { message: `Unknown event argument: ${unknown}.`, path: ['args', unknown] },
3586
+ ]),
3587
+ subscription,
3588
+ })
3589
+ break
3590
+ }
3591
+ for (const log of viemLogs) {
3592
+ const decoded = event ? decodeScanLog(log, event) : log
3593
+ const logIndex = decoded.logIndex ?? undefined
3594
+ if (logIndex === undefined) continue
3595
+ if (!matchesLog(decoded, f, event)) continue
3596
+ const data = toLogEvent(decoded, timestamp, f.signature, event)
3597
+ if (data) push(data, logIndex)
3598
+ }
3599
+ break
3600
+ }
3601
+ case 'token:transfer': {
3602
+ if (timestamp === undefined) break
3603
+ const f = filters as WebhookTransfer.Filters
3604
+ for (const log of logs) {
3605
+ const data = WebhookTransfer.decode(log, timestamp)
3606
+ if (!data || !WebhookTransfer.matches(data, f)) continue
3607
+ push(data, data.logIndex)
3608
+ }
3609
+ break
3610
+ }
3611
+ case 'transaction:included': {
3612
+ const f = filters as z.output<typeof schema.TxFilters>
3613
+ const includeCalls = f.includeCalls ?? true
3614
+ const transactions = Array.isArray(block['transactions'])
3615
+ ? (block['transactions'] as unknown[])
3616
+ : []
3617
+ const rawTimestamp = asString(block['timestamp'])
3618
+ for (let offset = 0; offset < transactions.length; offset++) {
3619
+ const transaction = transactions[offset]
3620
+ if (!transaction || typeof transaction !== 'object') continue
3621
+ const tx = transaction as Record<string, unknown>
3622
+ const transactionIndex = Value.hexToNumber(tx['transactionIndex']) ?? offset
3623
+ if (!matchesTx(tx, f, includeCalls, rawTimestamp, blockNumber ?? 0)) continue
3624
+ const parsed = Transactions.schema.Rpc.Transaction.safeParse(tx)
3625
+ if (!parsed.success) continue
3626
+ const humanized = Transactions.humanizeTransaction(parsed.data)
3627
+ // A transaction's own timestamp is seconds-only; the enclosing
3628
+ // block carries milliseconds.
3629
+ push(
3630
+ timestamp === undefined ? humanized : { ...humanized, timestamp },
3631
+ transactionIndex,
3632
+ )
3633
+ }
3634
+ break
3635
+ }
3636
+ }
3637
+ } catch (error) {
3638
+ matches.length = matched
3639
+ invalid.push({ error, subscription })
3640
+ }
3641
+ }
3642
+ return { invalid, matches }
3643
+ }
3644
+
3645
+ /** Normalizes a raw RPC header to the viem-shaped fields the block matchers read. */
3646
+ function toScanHeader(block: Record<string, unknown>): RpcBlock {
3647
+ const quantity = (value: unknown) => {
3648
+ try {
3649
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'bigint'
3650
+ ? BigInt(value)
3651
+ : undefined
3652
+ } catch {
3653
+ return undefined
3654
+ }
3655
+ }
3656
+ return {
3657
+ ...block,
3658
+ gasLimit: quantity(block['gasLimit']),
3659
+ gasUsed: quantity(block['gasUsed']),
3660
+ number: quantity(block['number']),
3661
+ timestamp: quantity(block['timestamp']),
3662
+ } as RpcBlock
3663
+ }
3664
+
3665
+ export declare namespace scanBlock {
3666
+ /** The subscription fields matching consumes; extra fields flow through. */
3667
+ type Subscription = {
3668
+ /** Event type selecting the matcher and payload shape. */
3669
+ eventType: EventType
3670
+ /** Stored filter predicates, validated fail-closed per event type. */
3671
+ filters: Record<string, unknown>
3672
+ }
3673
+
3674
+ /** One fetched block plus the subscriptions to match it against. */
3675
+ type Options<subscription extends Subscription> = {
3676
+ /** Raw `eth_getBlockByNumber(_, true)` result: header plus raw transactions. */
3677
+ block: Record<string, unknown>
3678
+ /** Raw `eth_getLogs` entries for exactly this block, in log order. */
3679
+ logs: readonly RpcLog[]
3680
+ /** Active subscriptions for the chain; any mix of event types. */
3681
+ subscriptions: readonly subscription[]
3682
+ }
3683
+
3684
+ /** One deliverable row for one subscription. */
3685
+ type Match<subscription extends Subscription> = {
3686
+ /** Block the row was produced from. */
3687
+ blockNumber: number
3688
+ /** Payload the matching read endpoint returns (pre-enrichment). */
3689
+ data: unknown
3690
+ /** Row position within the block (log or transaction index; 0 for blocks). */
3691
+ logIndex: number
3692
+ /** The matched subscription, passed through verbatim. */
3693
+ subscription: subscription
3694
+ }
3695
+
3696
+ /** A subscription whose stored filters failed validation. */
3697
+ type Invalid<subscription extends Subscription> = {
3698
+ /** The validation failure. */
3699
+ error: unknown
3700
+ /** The subscription with invalid filters. */
3701
+ subscription: subscription
3702
+ }
3703
+
3704
+ /** Matches plus fail-closed filter rejections. */
3705
+ type Result<subscription extends Subscription> = {
3706
+ /** Subscriptions skipped because their stored filters failed validation. */
3707
+ invalid: readonly Invalid<subscription>[]
3708
+ /** Deliverable rows in subscription order, block order within each type. */
3709
+ matches: readonly Match<subscription>[]
3710
+ }
3711
+ }
3712
+
3485
3713
  class BlockUnavailableError extends Error {
3486
3714
  override name = 'Webhooks.BlockUnavailableError'
3487
3715
 
@@ -0,0 +1,32 @@
1
+ import * as Tidx from '../../../internal/Tidx.js'
2
+ import * as Viem from '../../../internal/Viem.js'
3
+ import * as TestApp from '../../../../test/App.js'
4
+ import * as QueryBench from '../../../../test/QueryBench.js'
5
+
6
+ const senderTag = `0x${'00'.repeat(32)}`
7
+ const zoneChainIds = {
8
+ 4217: 421_700_002,
9
+ 42431: 424_310_002,
10
+ } as const
11
+
12
+ QueryBench.bench({
13
+ chainId: false,
14
+ name: 'listZoneWithdrawals',
15
+ path: {
16
+ 4217: `/v1/zones/withdrawals/${senderTag}?chainId=${zoneChainIds[4217]}`,
17
+ 42431: `/v1/zones/withdrawals/${senderTag}?chainId=${zoneChainIds[42431]}`,
18
+ },
19
+ zones: ({ chainId }) => {
20
+ const rpcUrl = Viem.resolveUrl(process.env.RPC_URL, chainId) ?? Viem.url[chainId]
21
+ const tidxUrl = Viem.resolveUrl(process.env.TIDX_URL, chainId) ?? Tidx.url[chainId]
22
+ if (!rpcUrl || !tidxUrl) throw new Error(`Missing Zone upstream for chain ${chainId}.`)
23
+ return [
24
+ TestApp.zone({
25
+ chainId: zoneChainIds[chainId],
26
+ rpcUrl,
27
+ sourceChainId: chainId,
28
+ tidxUrl,
29
+ }),
30
+ ]
31
+ },
32
+ })
@@ -419,7 +419,7 @@ describe('GET /v1/zones/withdrawals/:senderTag', () => {
419
419
  },
420
420
  {
421
421
  "examples": [
422
- 421700001,
422
+ 1424310001,
423
423
  ],
424
424
  "name": "chainId",
425
425
  "required": true,
@@ -43,7 +43,7 @@ export namespace schema {
43
43
  .strictObject({
44
44
  chainId: Schema.ChainId.check(
45
45
  z.describe('The chain id of the Zone where the withdrawal originated.'),
46
- z.meta({ examples: [421_700_001] }),
46
+ z.meta({ examples: [1_424_310_001] }),
47
47
  ),
48
48
  })
49
49
  .check(z.describe('Query parameters used to select the originating Zone.'))
@@ -22,7 +22,7 @@ const providers = [
22
22
  integratorId: '0xdead',
23
23
  userAddress: '0x1111111111111111111111111111111111111111',
24
24
  }),
25
- Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
25
+ Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
26
26
  Rhino.rhino(),
27
27
  Squid.squid({
28
28
  integratorId: 'squid-integrator-id',