tapimo 0.9.0 → 0.10.1

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 (319) 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 +4292 -3708
  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 +21 -20
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +66 -23
  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 +26 -17
  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/apps/verified-tokens.d.ts +168 -168
  24. package/dist/admin/ui.generated.d.ts +1 -1
  25. package/dist/admin/ui.generated.js +69 -69
  26. package/dist/admin/ui.generated.js.map +1 -1
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +26 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +4280 -84
  31. package/dist/apps/Zones.d.ts.map +1 -1
  32. package/dist/apps/Zones.js +30 -10
  33. package/dist/apps/Zones.js.map +1 -1
  34. package/dist/apps/data/App.d.ts +5799 -4929
  35. package/dist/apps/data/App.d.ts.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +4 -5
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +19 -4
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +3391 -492
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +372 -29
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +853 -339
  45. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.d.ts +10 -10
  47. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  48. package/dist/apps/data/routes/balances.js +145 -83
  49. package/dist/apps/data/routes/balances.js.map +1 -1
  50. package/dist/apps/data/routes/blocks.d.ts +123 -123
  51. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  52. package/dist/apps/data/routes/earn.d.ts +455 -1
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +369 -45
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  57. package/dist/apps/data/routes/exchanges.js +114 -118
  58. package/dist/apps/data/routes/exchanges.js.map +1 -1
  59. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  60. package/dist/apps/data/routes/indexer.d.ts +35 -34
  61. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  62. package/dist/apps/data/routes/indexer.js +3 -2
  63. package/dist/apps/data/routes/indexer.js.map +1 -1
  64. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  65. package/dist/apps/data/routes/receipts.js +68 -47
  66. package/dist/apps/data/routes/receipts.js.map +1 -1
  67. package/dist/apps/data/routes/rpc.d.ts +31 -31
  68. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  69. package/dist/apps/data/routes/rpc.js +5 -1
  70. package/dist/apps/data/routes/rpc.js.map +1 -1
  71. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  72. package/dist/apps/data/routes/tokens.d.ts +461 -459
  73. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  74. package/dist/apps/data/routes/transactions.d.ts +313 -305
  75. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  76. package/dist/apps/data/routes/transactions.js +25 -23
  77. package/dist/apps/data/routes/transactions.js.map +1 -1
  78. package/dist/apps/data/routes/transfers.d.ts +108 -86
  79. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  80. package/dist/apps/data/routes/transfers.js +618 -105
  81. package/dist/apps/data/routes/transfers.js.map +1 -1
  82. package/dist/apps/data/routes/valuation.d.ts +21 -21
  83. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  84. package/dist/apps/data/routes/valuation.js +2 -2
  85. package/dist/apps/data/routes/valuation.js.map +1 -1
  86. package/dist/apps/data/routes/webhooks.d.ts +519 -868
  87. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/data/routes/webhooks.js +642 -353
  89. package/dist/apps/data/routes/webhooks.js.map +1 -1
  90. package/dist/apps/data/routes/zones.d.ts +31 -31
  91. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  92. package/dist/apps/data/routes/zones.js +4 -2
  93. package/dist/apps/data/routes/zones.js.map +1 -1
  94. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  95. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  96. package/dist/apps/funding/routes/providers.d.ts +13 -13
  97. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  98. package/dist/apps/funding/routes/transfers.d.ts +36 -36
  99. package/dist/apps/funding/routes/transfers.js +1 -1
  100. package/dist/apps/funding/routes/transfers.js.map +1 -1
  101. package/dist/apps/management/App.d.ts +11 -309
  102. package/dist/apps/management/App.d.ts.map +1 -1
  103. package/dist/apps/management/routes/api-keys.d.ts +74 -74
  104. package/dist/apps/management/routes/billing.js +1 -1
  105. package/dist/apps/management/routes/billing.js.map +1 -1
  106. package/dist/apps/management/routes/faucet.d.ts +35 -35
  107. package/dist/apps/management/routes/faucet.js +2 -2
  108. package/dist/apps/management/routes/faucet.js.map +1 -1
  109. package/dist/apps/management/routes/invitations.d.ts +134 -134
  110. package/dist/apps/management/routes/invite-links.d.ts +80 -80
  111. package/dist/apps/management/routes/members.d.ts +69 -69
  112. package/dist/apps/management/routes/orgs.d.ts +98 -98
  113. package/dist/apps/management/routes/projects.d.ts +60 -60
  114. package/dist/apps/management/routes/usage.d.ts +72 -72
  115. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  116. package/dist/apps/management/routes/webhooks.d.ts +203 -618
  117. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  118. package/dist/apps/mcp.d.ts.map +1 -1
  119. package/dist/apps/mcp.js +4 -1
  120. package/dist/apps/mcp.js.map +1 -1
  121. package/dist/apps/mpp/App.d.ts +56 -44
  122. package/dist/apps/mpp/App.d.ts.map +1 -1
  123. package/dist/apps/mpp/Mpp.d.ts +3 -0
  124. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  125. package/dist/apps/mpp/Mpp.js +3 -0
  126. package/dist/apps/mpp/Mpp.js.map +1 -1
  127. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  128. package/dist/apps/mpp/internal/Adapter.js +9 -2
  129. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  130. package/dist/db/Db.d.ts +4 -2
  131. package/dist/db/Db.d.ts.map +1 -1
  132. package/dist/db/Db.js +75 -15
  133. package/dist/db/Db.js.map +1 -1
  134. package/dist/db/Schema.d.ts +19 -0
  135. package/dist/db/Schema.d.ts.map +1 -1
  136. package/dist/db/Schema.js +24 -0
  137. package/dist/db/Schema.js.map +1 -1
  138. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  139. package/dist/db/tables/sponsoredTransactions.js +4 -1
  140. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  141. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  142. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  143. package/dist/db/tables/webhookQueueEvents.js +160 -0
  144. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  145. package/dist/db/tables/webhookSubscriptions.d.ts +58 -3
  146. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  147. package/dist/db/tables/webhookSubscriptions.js +86 -3
  148. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  149. package/dist/internal/Auth.d.ts +3 -0
  150. package/dist/internal/Auth.d.ts.map +1 -1
  151. package/dist/internal/Auth.js +6 -4
  152. package/dist/internal/Auth.js.map +1 -1
  153. package/dist/internal/MetricSink.d.ts +139 -3
  154. package/dist/internal/MetricSink.d.ts.map +1 -1
  155. package/dist/internal/MetricSink.js +144 -4
  156. package/dist/internal/MetricSink.js.map +1 -1
  157. package/dist/internal/RateLimit.d.ts +4 -6
  158. package/dist/internal/RateLimit.d.ts.map +1 -1
  159. package/dist/internal/RateLimit.js +15 -14
  160. package/dist/internal/RateLimit.js.map +1 -1
  161. package/dist/internal/Schema.js +1 -1
  162. package/dist/internal/Schema.js.map +1 -1
  163. package/dist/internal/Store.d.ts +2 -1
  164. package/dist/internal/Store.d.ts.map +1 -1
  165. package/dist/internal/Store.js +57 -18
  166. package/dist/internal/Store.js.map +1 -1
  167. package/dist/internal/Tidx.d.ts +1 -3
  168. package/dist/internal/Tidx.d.ts.map +1 -1
  169. package/dist/internal/Tidx.js +1 -3
  170. package/dist/internal/Tidx.js.map +1 -1
  171. package/dist/internal/Value.d.ts +2 -0
  172. package/dist/internal/Value.d.ts.map +1 -1
  173. package/dist/internal/Value.js +25 -0
  174. package/dist/internal/Value.js.map +1 -1
  175. package/dist/internal/Viem.d.ts.map +1 -1
  176. package/dist/internal/Viem.js +2 -1
  177. package/dist/internal/Viem.js.map +1 -1
  178. package/dist/internal/WebhookDestination.d.ts +2 -0
  179. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  180. package/dist/internal/WebhookDestination.js +11 -0
  181. package/dist/internal/WebhookDestination.js.map +1 -1
  182. package/dist/internal/WebhookTransfer.d.ts +75 -0
  183. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  184. package/dist/internal/WebhookTransfer.js +299 -0
  185. package/dist/internal/WebhookTransfer.js.map +1 -0
  186. package/dist/internal/Webhooks.d.ts +176 -0
  187. package/dist/internal/Webhooks.d.ts.map +1 -1
  188. package/dist/internal/Webhooks.js +216 -20
  189. package/dist/internal/Webhooks.js.map +1 -1
  190. package/dist/internal/funding/Provider.d.ts +4 -0
  191. package/dist/internal/funding/Provider.d.ts.map +1 -1
  192. package/dist/internal/funding/Provider.js +11 -7
  193. package/dist/internal/funding/Provider.js.map +1 -1
  194. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  195. package/dist/internal/funding/Reconciliation.js +128 -0
  196. package/dist/internal/funding/Reconciliation.js.map +1 -1
  197. package/dist/internal/funding/providers/relay.d.ts +6 -4
  198. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  199. package/dist/internal/funding/providers/relay.js +71 -9
  200. package/dist/internal/funding/providers/relay.js.map +1 -1
  201. package/package.json +4 -4
  202. package/src/ApiKeys.test.ts +20 -0
  203. package/src/ApiKeys.ts +28 -1
  204. package/src/App.test.ts +10 -8
  205. package/src/App.ts +1 -0
  206. package/src/Client.test-d.ts +61 -5
  207. package/src/Webhooks.bench.ts +714 -0
  208. package/src/Webhooks.memory.bench.ts +250 -0
  209. package/src/Webhooks.test.ts +77 -6
  210. package/src/Webhooks.ts +85 -29
  211. package/src/admin/apps/api-keys.test.ts +9 -0
  212. package/src/admin/apps/api-keys.ts +15 -0
  213. package/src/admin/ui/src/lib/query-client.ts +3 -2
  214. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  215. package/src/admin/ui.generated.ts +69 -69
  216. package/src/analytics/Analytics.test.ts +8 -0
  217. package/src/analytics/Analytics.ts +26 -0
  218. package/src/apps/Zones.test.ts +23 -5
  219. package/src/apps/Zones.ts +31 -10
  220. package/src/apps/composition.test.ts +1 -0
  221. package/src/apps/data/App.test.ts +2 -2
  222. package/src/apps/data/FxOracle.test.ts +18 -0
  223. package/src/apps/data/FxOracle.ts +28 -5
  224. package/src/apps/data/routes/activities.bench.ts +41 -0
  225. package/src/apps/data/routes/activities.test.ts +122 -2
  226. package/src/apps/data/routes/activities.ts +435 -32
  227. package/src/apps/data/routes/balances.bench.ts +21 -0
  228. package/src/apps/data/routes/balances.test.ts +119 -10
  229. package/src/apps/data/routes/balances.ts +164 -97
  230. package/src/apps/data/routes/blocks.bench.ts +34 -0
  231. package/src/apps/data/routes/blocks.test.ts +12 -6
  232. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  233. package/src/apps/data/routes/earn.bench.ts +71 -0
  234. package/src/apps/data/routes/earn.test.ts +197 -0
  235. package/src/apps/data/routes/earn.ts +487 -45
  236. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  237. package/src/apps/data/routes/exchanges.test.ts +7 -6
  238. package/src/apps/data/routes/exchanges.ts +124 -117
  239. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  240. package/src/apps/data/routes/indexer.bench.ts +8 -0
  241. package/src/apps/data/routes/indexer.test.ts +2 -1
  242. package/src/apps/data/routes/indexer.ts +3 -2
  243. package/src/apps/data/routes/receipts.bench.ts +39 -0
  244. package/src/apps/data/routes/receipts.test.ts +24 -7
  245. package/src/apps/data/routes/receipts.ts +67 -50
  246. package/src/apps/data/routes/rpc.bench.ts +14 -0
  247. package/src/apps/data/routes/rpc.test.ts +27 -0
  248. package/src/apps/data/routes/rpc.ts +5 -1
  249. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  250. package/src/apps/data/routes/tokens.bench.ts +76 -0
  251. package/src/apps/data/routes/tokens.test.ts +51 -0
  252. package/src/apps/data/routes/transactions.bench.ts +40 -0
  253. package/src/apps/data/routes/transactions.test.ts +42 -0
  254. package/src/apps/data/routes/transactions.ts +44 -39
  255. package/src/apps/data/routes/transfers.bench.ts +28 -0
  256. package/src/apps/data/routes/transfers.test.ts +402 -24
  257. package/src/apps/data/routes/transfers.ts +708 -101
  258. package/src/apps/data/routes/valuation.bench.ts +14 -0
  259. package/src/apps/data/routes/valuation.test.ts +26 -0
  260. package/src/apps/data/routes/valuation.ts +6 -2
  261. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  262. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  263. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  264. package/src/apps/data/routes/webhooks.test.ts +466 -135
  265. package/src/apps/data/routes/webhooks.ts +820 -429
  266. package/src/apps/data/routes/zones.bench.ts +32 -0
  267. package/src/apps/data/routes/zones.test.ts +1 -1
  268. package/src/apps/data/routes/zones.ts +7 -2
  269. package/src/apps/funding/routes/chains.test.ts +1 -1
  270. package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
  271. package/src/apps/funding/routes/deposit-addresses.ts +1 -1
  272. package/src/apps/funding/routes/providers.test.ts +1 -1
  273. package/src/apps/funding/routes/quotes.test.ts +2 -2
  274. package/src/apps/funding/routes/transfers.test.ts +3 -6
  275. package/src/apps/funding/routes/transfers.ts +1 -1
  276. package/src/apps/management/routes/billing.ts +1 -1
  277. package/src/apps/management/routes/faucet.ts +2 -2
  278. package/src/apps/management/routes/me.test.ts +31 -0
  279. package/src/apps/mcp.ts +4 -1
  280. package/src/apps/mpp/Mpp.test.ts +13 -1
  281. package/src/apps/mpp/Mpp.ts +8 -0
  282. package/src/apps/mpp/internal/Adapter.ts +10 -2
  283. package/src/db/Db.test.ts +40 -6
  284. package/src/db/Db.ts +114 -19
  285. package/src/db/Schema.ts +32 -0
  286. package/src/db/tables/sponsoredTransactions.ts +9 -2
  287. package/src/db/tables/webhookQueueEvents.ts +285 -0
  288. package/src/db/tables/webhookSubscriptions.ts +131 -4
  289. package/src/internal/Auth.test.ts +61 -50
  290. package/src/internal/Auth.ts +9 -4
  291. package/src/internal/EarnVaults.test.ts +2 -2
  292. package/src/internal/MetricSink.test.ts +575 -0
  293. package/src/internal/MetricSink.ts +325 -10
  294. package/src/internal/RateLimit.test.ts +23 -18
  295. package/src/internal/RateLimit.ts +23 -15
  296. package/src/internal/Schema.ts +1 -1
  297. package/src/internal/Store.test.ts +99 -13
  298. package/src/internal/Store.ts +52 -17
  299. package/src/internal/Tidx.test.ts +4 -4
  300. package/src/internal/Tidx.ts +1 -3
  301. package/src/internal/Value.test.ts +25 -0
  302. package/src/internal/Value.ts +29 -0
  303. package/src/internal/Viem.test.ts +8 -0
  304. package/src/internal/Viem.ts +2 -1
  305. package/src/internal/WebhookDestination.test.ts +20 -0
  306. package/src/internal/WebhookDestination.ts +10 -0
  307. package/src/internal/WebhookTransfer.test.ts +89 -0
  308. package/src/internal/WebhookTransfer.ts +430 -0
  309. package/src/internal/Webhooks.test.ts +616 -0
  310. package/src/internal/Webhooks.ts +412 -24
  311. package/src/internal/funding/Provider.test.ts +48 -8
  312. package/src/internal/funding/Provider.ts +12 -7
  313. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  314. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  315. package/src/internal/funding/Reconciliation.ts +136 -0
  316. package/src/internal/funding/SourceObservation.test.ts +1 -0
  317. package/src/internal/funding/providers/index.test.ts +73 -3
  318. package/src/internal/funding/providers/relay.test.ts +2 -9
  319. package/src/internal/funding/providers/relay.ts +72 -13
@@ -1,8 +1,7 @@
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'
5
- import * as Tidx from '../../../internal/Tidx.js'
6
5
  import * as Viem from '../../../internal/Viem.js'
7
6
  import * as TestApp from '../../../../test/App.js'
8
7
  import * as Mppx from '../../../../test/Mppx.js'
@@ -1209,16 +1208,13 @@ describe('OpenAPI visibility', () => {
1209
1208
  "from",
1210
1209
  "to",
1211
1210
  "includeCalls",
1212
- "includeReceipt",
1213
1211
  "value",
1214
1212
  "input",
1215
1213
  "calls",
1216
1214
  "callCount",
1217
1215
  "txType",
1218
1216
  "feeToken",
1219
- "feePayer",
1220
1217
  "gasLimit",
1221
- "gasUsed",
1222
1218
  "maxFeePerGas",
1223
1219
  "maxPriorityFeePerGas",
1224
1220
  "nonce",
@@ -1227,7 +1223,6 @@ describe('OpenAPI visibility', () => {
1227
1223
  "validAfter",
1228
1224
  "blockNumber",
1229
1225
  "timestamp",
1230
- "status",
1231
1226
  ],
1232
1227
  "title": "Included-transaction subscription",
1233
1228
  },
@@ -1261,6 +1256,10 @@ describe('OpenAPI visibility', () => {
1261
1256
  })
1262
1257
 
1263
1258
  describe('envelope schema', () => {
1259
+ test('transaction events exclude receipt metadata', () => {
1260
+ expect(Object.keys(Webhooks.schema.TransactionEvent.shape.meta.shape)).toEqual(['rpc'])
1261
+ })
1262
+
1264
1263
  /** A `transfer` subscription used to build representative envelopes. */
1265
1264
  async function transferSubscription() {
1266
1265
  const db = TestApp.database()
@@ -1352,10 +1351,9 @@ describe('MPP access (disabled for now — API-key-only)', () => {
1352
1351
  })
1353
1352
  })
1354
1353
 
1355
- // The poll tests pin a FIXED immutable historical window on testnet chain 42431
1356
- // (real TIDX + `rpc.testnet.tempo.xyz`). Under `localnet`, the test RPC client is
1357
- // redirected to the disposable local chain, which lacks that history, so the
1358
- // whole block is testnet-only.
1354
+ // The poll tests pin a fixed immutable historical window on testnet chain 42431.
1355
+ // Under `localnet`, the test RPC client is redirected to the disposable local
1356
+ // chain, which lacks that history, so the whole block is testnet-only.
1359
1357
  describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1360
1358
  /** Decodes an opaque creation checkpoint or `(block, index)` poller cursor. */
1361
1359
  function decodeCursor(token: string | null): number[] | null {
@@ -1372,17 +1370,8 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1372
1370
  /** End-of-block index sentinel (matches the poller's own cursor sentinel). */
1373
1371
  const endOfBlockIndex = 2_147_483_647
1374
1372
 
1375
- // The poller scans real TIDX, so the tests pin a FIXED historical block window
1376
- // on chain 42431. Chain history is immutable, so a narrow `sender` filter over
1377
- // this window yields a deterministic, snapshot-stable row set while exercising
1378
- // the real keyset/`toBlock` SQL and decode path end to end — no fabricated rows.
1379
- const realTidx = Tidx.getClient({
1380
- chainId: 42431,
1381
- tidx: { auth: process.env.TIDX_AUTH, baseUrl: 'https://tidx.tempo.xyz' },
1382
- })
1383
- // The monitoring scanners (`log:emitted`/`block:created`/`transaction:included`)
1384
- // read the same immutable historical window over real RPC (chain 42431 →
1385
- // `rpc.testnet.tempo.xyz`), so their results are snapshot-stable too.
1373
+ // Every scanner reads this immutable window over real RPC (chain 42431 to
1374
+ // `rpc.testnet.tempo.xyz`), so the results remain snapshot-stable.
1386
1375
  const client = Viem.getClient({ chainId: 42431, rpc: TestApp.rpc })
1387
1376
  // Confirmed scan window `[windowFrom, windowTo]`: anchor the cursor just below
1388
1377
  // `windowFrom`, and pass `headBlockNumber = windowTo + confirmations` so the
@@ -1390,10 +1379,11 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1390
1379
  const windowFrom = 1_000_000
1391
1380
  const windowTo = 1_000_002
1392
1381
  const windowCursor = encodeCursor([windowFrom - 1, endOfBlockIndex])
1393
- const windowHead = windowTo + 2
1382
+ const windowHead = windowTo + 1
1394
1383
  // A sender whose only transfers in `[windowFrom, windowTo]` are exactly two
1395
1384
  // rows, both in block 1,000,002 (`log_idx` 0 then 1): a TIP-20 transfer
1396
1385
  // followed by its fee payment. Filtering on it makes the scan deterministic.
1386
+ const windowRecipient = '0x9e39034aae71fb89f66061a2602eb6efec271754'
1397
1387
  const windowSender = '0xe7687128b0a808c2831ff94d4f7b2fb35c65af38'
1398
1388
  // This token has 226 transfers across blocks 1,000,000 through 1,000,003.
1399
1389
  const volumeHead = 1_000_005
@@ -1428,17 +1418,28 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1428
1418
  await Core.setCursor(db, subscription.id, windowCursor)
1429
1419
 
1430
1420
  const dispatched: Webhooks.poll.Dispatchable[] = []
1421
+ const scanCheckpoints: Webhooks.poll.ScanCheckpoint[] = []
1422
+ const stages: Webhooks.poll.Stage[] = []
1423
+ let cursorLagBlocks: number | undefined
1431
1424
  const result = await Webhooks.poll({
1432
1425
  chainId: 42431,
1433
1426
  dispatch: async (input) => {
1434
1427
  dispatched.push(input)
1435
1428
  return { ok: true }
1436
1429
  },
1437
- eventTypes: ['token:transfer'],
1430
+ eventTypes: ['token:transfer', 'token:transfer'],
1438
1431
  headBlockNumber: windowHead,
1439
1432
  client,
1440
1433
  db,
1441
- tidx: realTidx,
1434
+ onCursorLag(blocks) {
1435
+ cursorLagBlocks = blocks
1436
+ },
1437
+ onScanCheckpoint(checkpoint) {
1438
+ scanCheckpoints.push(checkpoint)
1439
+ },
1440
+ onStage(stage) {
1441
+ stages.push(stage)
1442
+ },
1442
1443
  })
1443
1444
 
1444
1445
  expect(result).toMatchInlineSnapshot(`
@@ -1451,9 +1452,68 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1451
1452
  "subscriptions": 1,
1452
1453
  }
1453
1454
  `)
1455
+ expect(cursorLagBlocks).toBe(3)
1456
+ expect(scanCheckpoints.map(({ durationMs: _, ...checkpoint }) => checkpoint))
1457
+ .toMatchInlineSnapshot(`
1458
+ [
1459
+ {
1460
+ "concurrency": 8,
1461
+ "eventType": "token:transfer",
1462
+ "maxWindowBlocks": 3,
1463
+ "phase": "start",
1464
+ "rpcMethod": "eth_getLogs",
1465
+ "subscriptionCount": 1,
1466
+ "subscriptionLimit": 256,
1467
+ "totalWindowBlocks": 3,
1468
+ },
1469
+ {
1470
+ "concurrency": 8,
1471
+ "eventType": "token:transfer",
1472
+ "failedSubscriptions": 0,
1473
+ "matchedItems": 2,
1474
+ "maxWindowBlocks": 3,
1475
+ "outcome": "ok",
1476
+ "phase": "complete",
1477
+ "rpcMethod": "eth_getLogs",
1478
+ "subscriptionCount": 1,
1479
+ "subscriptionLimit": 256,
1480
+ "totalWindowBlocks": 3,
1481
+ },
1482
+ ]
1483
+ `)
1484
+ expect(stages.map(({ eventType, outcome, stage }) => ({ eventType, outcome, stage })))
1485
+ .toMatchInlineSnapshot(`
1486
+ [
1487
+ {
1488
+ "eventType": undefined,
1489
+ "outcome": "ok",
1490
+ "stage": "rpc_head",
1491
+ },
1492
+ {
1493
+ "eventType": undefined,
1494
+ "outcome": "ok",
1495
+ "stage": "subscription_read",
1496
+ },
1497
+ {
1498
+ "eventType": "token:transfer",
1499
+ "outcome": "ok",
1500
+ "stage": "scan",
1501
+ },
1502
+ {
1503
+ "eventType": "token:transfer",
1504
+ "outcome": "ok",
1505
+ "stage": "enrich",
1506
+ },
1507
+ {
1508
+ "eventType": "token:transfer",
1509
+ "outcome": "ok",
1510
+ "stage": "cursor_write",
1511
+ },
1512
+ ]
1513
+ `)
1454
1514
  expect(dispatched).toHaveLength(2)
1455
- // The envelope carries the read-endpoint transfer row verbatim here the
1456
- // first of the two pinned-window transfers from `windowSender`.
1515
+ // The envelope carries the decoded RPC transfer row, here the first of the
1516
+ // two pinned-window transfers from `windowSender`.
1457
1517
  expect(dispatched[0]?.envelope.data).toMatchInlineSnapshot(`
1458
1518
  {
1459
1519
  "address": "0x20c00000000000000000000071de0cd31ab0d105",
@@ -1462,7 +1522,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1462
1522
  "logIndex": 0,
1463
1523
  "recipient": "0x9e39034aae71fb89f66061a2602eb6efec271754",
1464
1524
  "sender": "0xe7687128b0a808c2831ff94d4f7b2fb35c65af38",
1465
- "timestamp": "2026-01-14T18:38:03.000Z",
1525
+ "timestamp": "2026-01-14T18:38:03.685Z",
1466
1526
  "token": {
1467
1527
  "decimals": 6,
1468
1528
  "symbol": "QKSUSD",
@@ -1480,6 +1540,221 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1480
1540
  `)
1481
1541
  })
1482
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
+
1589
+ test('scans event types concurrently without increasing scan bounds', async () => {
1590
+ const db = TestApp.database()
1591
+ const transfer = await createTransferSubscription(db)
1592
+ const block = await Core.createSubscription(db, {
1593
+ chainId: 42431,
1594
+ destination: { type: 'url', url: 'https://hooks.example.com/block' },
1595
+ eventType: 'block:created',
1596
+ filters: {},
1597
+ owner: { orgId: 'org_block', type: 'api_key' },
1598
+ })
1599
+ await Promise.all([
1600
+ Core.setCursor(db, transfer.id, windowCursor),
1601
+ Core.setCursor(db, block.id, windowCursor),
1602
+ ])
1603
+
1604
+ const active = new Map<
1605
+ Webhooks.EventType,
1606
+ Pick<Webhooks.poll.ScanCheckpoint, 'concurrency' | 'subscriptionLimit'>
1607
+ >()
1608
+ let maxConcurrency = 0
1609
+ let maxEventTypes = 0
1610
+ let maxSubscriptionLimit = 0
1611
+ await Webhooks.poll({
1612
+ chainId: 42431,
1613
+ client,
1614
+ db,
1615
+ dispatch: async () => ({ ok: true }),
1616
+ eventTypes: ['token:transfer', 'block:created'],
1617
+ headBlockNumber: windowHead,
1618
+ onScanCheckpoint(checkpoint) {
1619
+ expect(checkpoint.subscriptionCount).toBeLessThanOrEqual(checkpoint.subscriptionLimit)
1620
+ if (checkpoint.phase === 'start')
1621
+ active.set(checkpoint.eventType, {
1622
+ concurrency: checkpoint.concurrency,
1623
+ subscriptionLimit: checkpoint.subscriptionLimit,
1624
+ })
1625
+ else active.delete(checkpoint.eventType)
1626
+ maxConcurrency = Math.max(
1627
+ maxConcurrency,
1628
+ ...[...active.values()].map((value) => value.concurrency),
1629
+ )
1630
+ maxEventTypes = Math.max(maxEventTypes, active.size)
1631
+ maxSubscriptionLimit = Math.max(
1632
+ maxSubscriptionLimit,
1633
+ [...active.values()].reduce((total, value) => total + value.subscriptionLimit, 0),
1634
+ )
1635
+ },
1636
+ })
1637
+
1638
+ expect(active.size).toBe(0)
1639
+ expect(maxConcurrency).toBe(8)
1640
+ expect(maxEventTypes).toBe(2)
1641
+ expect(maxSubscriptionLimit).toBe(256)
1642
+ })
1643
+
1644
+ test('dispatches a page as one batch and advances the cursor', async () => {
1645
+ const db = TestApp.database()
1646
+ const subscription = await createTransferSubscription(db)
1647
+ await Core.setCursor(db, subscription.id, windowCursor)
1648
+
1649
+ const batches: Webhooks.poll.Dispatchable[][] = []
1650
+ const result = await Webhooks.poll({
1651
+ chainId: 42431,
1652
+ client,
1653
+ db,
1654
+ dispatchBatch: async (dispatchables, context) => {
1655
+ expect(context.db).toBe(db)
1656
+ batches.push([...dispatchables])
1657
+ return { ok: true }
1658
+ },
1659
+ eventTypes: ['token:transfer'],
1660
+ headBlockNumber: windowHead,
1661
+ })
1662
+
1663
+ expect(result.dispatched).toBe(2)
1664
+ expect(batches.map((batch) => batch.length)).toEqual([2])
1665
+ expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
1666
+ })
1667
+
1668
+ test('keeps the cursor unchanged when a batch dispatch fails', async () => {
1669
+ const db = TestApp.database()
1670
+ const subscription = await createTransferSubscription(db)
1671
+ await Core.setCursor(db, subscription.id, windowCursor)
1672
+
1673
+ const result = await Webhooks.poll({
1674
+ chainId: 42431,
1675
+ client,
1676
+ db,
1677
+ dispatchBatch: async () => ({ error: 'unreachable', ok: false }),
1678
+ eventTypes: ['token:transfer'],
1679
+ headBlockNumber: windowHead,
1680
+ })
1681
+
1682
+ expect(result.dispatched).toBe(0)
1683
+ expect(result.failed).toBe(2)
1684
+ expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
1685
+ windowFrom - 1,
1686
+ endOfBlockIndex,
1687
+ ])
1688
+ })
1689
+
1690
+ test('matches the either-side address filter through Viem log filters', async () => {
1691
+ const db = TestApp.database()
1692
+ const subscription = await Core.createSubscription(db, {
1693
+ chainId: 42431,
1694
+ destination: { type: 'url', url: 'https://hooks.example.com/address' },
1695
+ eventType: 'token:transfer',
1696
+ filters: { address: windowSender },
1697
+ owner: { orgId: 'org_address', type: 'api_key' },
1698
+ })
1699
+ await Core.setCursor(db, subscription.id, windowCursor)
1700
+
1701
+ const positions: string[] = []
1702
+ const result = await Webhooks.poll({
1703
+ chainId: 42431,
1704
+ client,
1705
+ db,
1706
+ dispatch: async ({ envelope }) => {
1707
+ const data = envelope.data as { blockNumber: number; logIndex: number }
1708
+ positions.push(`${data.blockNumber}:${data.logIndex}`)
1709
+ return { ok: true }
1710
+ },
1711
+ eventTypes: ['token:transfer'],
1712
+ headBlockNumber: windowHead,
1713
+ })
1714
+
1715
+ expect(result.dispatched).toBe(2)
1716
+ expect(positions).toEqual(['1000002:0', '1000002:1'])
1717
+ })
1718
+
1719
+ test('keeps distinct transfer pushdowns isolated', async () => {
1720
+ const db = TestApp.database()
1721
+ const recipient = await Core.createSubscription(db, {
1722
+ chainId: 42431,
1723
+ destination: { type: 'url', url: 'https://hooks.example.com/recipient' },
1724
+ eventType: 'token:transfer',
1725
+ filters: { recipient: windowRecipient },
1726
+ owner: { orgId: 'org_recipient', type: 'api_key' },
1727
+ })
1728
+ const sender = await createTransferSubscription(db)
1729
+ await Promise.all([
1730
+ Core.setCursor(db, recipient.id, windowCursor),
1731
+ Core.setCursor(db, sender.id, windowCursor),
1732
+ ])
1733
+
1734
+ const dispatched: Webhooks.poll.Dispatchable[] = []
1735
+ const result = await Webhooks.poll({
1736
+ chainId: 42431,
1737
+ client,
1738
+ db,
1739
+ dispatch: async (input) => {
1740
+ dispatched.push(input)
1741
+ return { ok: true }
1742
+ },
1743
+ eventTypes: ['token:transfer'],
1744
+ headBlockNumber: windowHead,
1745
+ })
1746
+
1747
+ expect(result.dispatched).toBe(3)
1748
+ expect(
1749
+ Object.fromEntries(
1750
+ [...Map.groupBy(dispatched, ({ subscription }) => subscription.id)].map(([id, items]) => [
1751
+ id,
1752
+ items.length,
1753
+ ]),
1754
+ ),
1755
+ ).toEqual({ [recipient.id]: 1, [sender.id]: 2 })
1756
+ })
1757
+
1483
1758
  test('pauses the cursor at the last success when dispatch fails', async () => {
1484
1759
  const db = TestApp.database()
1485
1760
  const subscription = await createTransferSubscription(db)
@@ -1494,7 +1769,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1494
1769
  headBlockNumber: windowHead,
1495
1770
  client,
1496
1771
  db,
1497
- tidx: realTidx,
1498
1772
  })
1499
1773
 
1500
1774
  expect(result.dispatched).toMatchInlineSnapshot(`1`)
@@ -1522,7 +1796,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1522
1796
  headBlockNumber: windowHead,
1523
1797
  client,
1524
1798
  db,
1525
- tidx: realTidx,
1526
1799
  })
1527
1800
 
1528
1801
  expect(result.dispatched).toMatchInlineSnapshot(`0`)
@@ -1537,6 +1810,31 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1537
1810
  `)
1538
1811
  })
1539
1812
 
1813
+ test('stops dispatching when the poll signal is aborted', async () => {
1814
+ const db = TestApp.database()
1815
+ const subscription = await createTransferSubscription(db)
1816
+ await Core.setCursor(db, subscription.id, windowCursor)
1817
+
1818
+ const abort = new AbortController()
1819
+ let calls = 0
1820
+ await expect(
1821
+ Webhooks.poll({
1822
+ chainId: 42431,
1823
+ client,
1824
+ db,
1825
+ dispatch: async () => {
1826
+ calls++
1827
+ abort.abort(new Error('lease lost'))
1828
+ return { ok: true }
1829
+ },
1830
+ eventTypes: ['token:transfer'],
1831
+ headBlockNumber: windowHead,
1832
+ signal: abort.signal,
1833
+ }),
1834
+ ).rejects.toThrow('lease lost')
1835
+ expect(calls).toBe(1)
1836
+ })
1837
+
1540
1838
  test('drains two bounded transfer pages per tick without gaps', async () => {
1541
1839
  const db = TestApp.database()
1542
1840
  const subscription = await createVolumeSubscription(db)
@@ -1547,6 +1845,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1547
1845
  Webhooks.poll({
1548
1846
  chainId: 42431,
1549
1847
  client,
1848
+ confirmations: 2,
1550
1849
  db,
1551
1850
  dispatch: async ({ envelope }) => {
1552
1851
  const data = envelope.data as { blockNumber: number; logIndex: number }
@@ -1555,7 +1854,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1555
1854
  },
1556
1855
  eventTypes: ['token:transfer'],
1557
1856
  headBlockNumber: volumeHead,
1558
- tidx: realTidx,
1559
1857
  })
1560
1858
 
1561
1859
  expect((await tick()).dispatched).toBe(200)
@@ -1577,11 +1875,11 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1577
1875
  const first = await Webhooks.poll({
1578
1876
  chainId: 42431,
1579
1877
  client,
1878
+ confirmations: 2,
1580
1879
  db,
1581
1880
  dispatch: async () => (++calls === 101 ? { error: 'unreachable', ok: false } : { ok: true }),
1582
1881
  eventTypes: ['token:transfer'],
1583
1882
  headBlockNumber: volumeHead,
1584
- tidx: realTidx,
1585
1883
  })
1586
1884
 
1587
1885
  expect(first.dispatched).toBe(100)
@@ -1593,6 +1891,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1593
1891
  batchSize: 1,
1594
1892
  chainId: 42431,
1595
1893
  client,
1894
+ confirmations: 2,
1596
1895
  db,
1597
1896
  dispatch: async ({ envelope }) => {
1598
1897
  const data = envelope.data as { blockNumber: number; logIndex: number }
@@ -1601,7 +1900,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1601
1900
  },
1602
1901
  eventTypes: ['token:transfer'],
1603
1902
  headBlockNumber: volumeHead,
1604
- tidx: realTidx,
1605
1903
  })
1606
1904
  expect(resumed).toEqual([1_000_002, 10])
1607
1905
  })
@@ -1618,6 +1916,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1618
1916
  await Webhooks.poll({
1619
1917
  chainId: 42431,
1620
1918
  client,
1919
+ confirmations: 2,
1621
1920
  db,
1622
1921
  dispatch: async ({ subscription }) => {
1623
1922
  order.push(subscription.id)
@@ -1625,7 +1924,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1625
1924
  },
1626
1925
  eventTypes: ['token:transfer'],
1627
1926
  headBlockNumber: volumeHead,
1628
- tidx: realTidx,
1629
1927
  })
1630
1928
 
1631
1929
  expect(order).toHaveLength(400)
@@ -1651,7 +1949,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1651
1949
  client,
1652
1950
  db,
1653
1951
  // Rows exist in history, but a brand-new subscription must not replay them.
1654
- tidx: realTidx,
1655
1952
  })
1656
1953
 
1657
1954
  // Nothing scanned or dispatched on the first tick; the cursor is anchored at
@@ -1689,7 +1986,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1689
1986
  headBlockNumber: creationHead + 3,
1690
1987
  client,
1691
1988
  db,
1692
- tidx: realTidx,
1693
1989
  })
1694
1990
 
1695
1991
  expect(result.dispatched).toBe(2)
@@ -1729,15 +2025,14 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1729
2025
  headBlockNumber: windowHead,
1730
2026
  client,
1731
2027
  db,
1732
- tidx: realTidx,
1733
2028
  })
1734
2029
 
1735
2030
  expect(decodeCursor(await Core.getCursor(db, transfer.id))).toEqual([
1736
- startBlockNumber - 2,
2031
+ startBlockNumber - 1,
1737
2032
  endOfBlockIndex,
1738
2033
  ])
1739
2034
  expect(decodeCursor(await Core.getCursor(db, block.id))).toEqual([
1740
- startBlockNumber - 2,
2035
+ startBlockNumber - 1,
1741
2036
  endOfBlockIndex,
1742
2037
  ])
1743
2038
  })
@@ -1754,10 +2049,9 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1754
2049
  chainId: 42431,
1755
2050
  dispatch: async () => ({ ok: true }),
1756
2051
  eventTypes: ['token:transfer'],
1757
- headBlockNumber: windowTo + 4,
2052
+ headBlockNumber: windowTo + 3,
1758
2053
  client,
1759
2054
  db,
1760
- tidx: realTidx,
1761
2055
  })
1762
2056
 
1763
2057
  expect(result.scanned).toMatchInlineSnapshot(`0`)
@@ -1779,6 +2073,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1779
2073
  const transferSignature =
1780
2074
  'event Transfer(address indexed from, address indexed to, uint256 value)'
1781
2075
  const transferSelector = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
2076
+ const windowToken = '0x20c00000000000000000000071de0cd31ab0d105'
1782
2077
  const windowSenderTopic = `0x${'0'.repeat(24)}${windowSender.slice(2)}`
1783
2078
 
1784
2079
  test('log:emitted decodes args from a signature-matched event view', async () => {
@@ -1803,7 +2098,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1803
2098
  headBlockNumber: windowHead,
1804
2099
  client,
1805
2100
  db,
1806
- tidx: realTidx,
1807
2101
  })
1808
2102
 
1809
2103
  expect(result.scanned).toMatchInlineSnapshot(`2`)
@@ -1823,7 +2117,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1823
2117
  "topic0": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
1824
2118
  },
1825
2119
  "logIndex": 0,
1826
- "timestamp": "2026-01-14T18:38:03.000Z",
2120
+ "timestamp": "2026-01-14T18:38:03.685Z",
1827
2121
  "topics": [
1828
2122
  "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
1829
2123
  "0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
@@ -1857,7 +2151,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1857
2151
  headBlockNumber: windowHead,
1858
2152
  client,
1859
2153
  db,
1860
- tidx: realTidx,
1861
2154
  })
1862
2155
 
1863
2156
  expect(result.scanned).toMatchInlineSnapshot(`2`)
@@ -1867,7 +2160,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1867
2160
  "blockNumber": 1000002,
1868
2161
  "data": "0x0000000000000000000000000000000000000000000000000000000000002710",
1869
2162
  "logIndex": 0,
1870
- "timestamp": "2026-01-14T18:38:03.000Z",
2163
+ "timestamp": "2026-01-14T18:38:03.685Z",
1871
2164
  "topics": [
1872
2165
  "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
1873
2166
  "0x000000000000000000000000e7687128b0a808c2831ff94d4f7b2fb35c65af38",
@@ -1879,6 +2172,87 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1879
2172
  `)
1880
2173
  })
1881
2174
 
2175
+ test('log:emitted scans an address-only filter', async () => {
2176
+ const db = TestApp.database()
2177
+ const subscription = await Core.createSubscription(db, {
2178
+ chainId: 42431,
2179
+ destination: { type: 'url', url: 'https://hooks.example.com/address-only' },
2180
+ eventType: 'log:emitted',
2181
+ filters: { address: '0xffffffffffffffffffffffffffffffffffffffff' },
2182
+ owner: { orgId: 'org_address_only', type: 'api_key' },
2183
+ })
2184
+ await Core.setCursor(db, subscription.id, windowCursor)
2185
+
2186
+ const result = await Webhooks.poll({
2187
+ chainId: 42431,
2188
+ client,
2189
+ db,
2190
+ eventTypes: ['log:emitted'],
2191
+ headBlockNumber: windowHead,
2192
+ })
2193
+
2194
+ expect(result.scanned).toBe(0)
2195
+ expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
2196
+ windowTo,
2197
+ endOfBlockIndex,
2198
+ ])
2199
+ })
2200
+
2201
+ test('log:emitted keeps distinct address and topic pushdowns isolated', async () => {
2202
+ const db = TestApp.database()
2203
+ const address = await Core.createSubscription(db, {
2204
+ chainId: 42431,
2205
+ destination: { type: 'url', url: 'https://hooks.example.com/address' },
2206
+ eventType: 'log:emitted',
2207
+ filters: {
2208
+ address: windowToken,
2209
+ args: { from: windowSender },
2210
+ signature: transferSignature,
2211
+ },
2212
+ owner: { orgId: 'org_address', type: 'api_key' },
2213
+ })
2214
+ const topic = await Core.createSubscription(db, {
2215
+ chainId: 42431,
2216
+ destination: { type: 'url', url: 'https://hooks.example.com/topic' },
2217
+ eventType: 'log:emitted',
2218
+ filters: { topic0: transferSelector, topic1: windowSenderTopic },
2219
+ owner: { orgId: 'org_topic', type: 'api_key' },
2220
+ })
2221
+ await Promise.all([
2222
+ Core.setCursor(db, address.id, windowCursor),
2223
+ Core.setCursor(db, topic.id, windowCursor),
2224
+ ])
2225
+
2226
+ const dispatched: Webhooks.poll.Dispatchable[] = []
2227
+ const result = await Webhooks.poll({
2228
+ chainId: 42431,
2229
+ client,
2230
+ db,
2231
+ dispatch: async (input) => {
2232
+ dispatched.push(input)
2233
+ return { ok: true }
2234
+ },
2235
+ eventTypes: ['log:emitted'],
2236
+ headBlockNumber: windowHead,
2237
+ })
2238
+
2239
+ expect(result.dispatched).toBe(3)
2240
+ expect(
2241
+ Object.fromEntries(
2242
+ [...Map.groupBy(dispatched, ({ subscription }) => subscription.id)].map(([id, items]) => [
2243
+ id,
2244
+ items.length,
2245
+ ]),
2246
+ ),
2247
+ ).toEqual({ [address.id]: 1, [topic.id]: 2 })
2248
+ expect(
2249
+ await Promise.all([Core.getCursor(db, address.id), Core.getCursor(db, topic.id)]),
2250
+ ).toEqual([
2251
+ encodeCursor([windowTo, endOfBlockIndex]),
2252
+ encodeCursor([windowTo, endOfBlockIndex]),
2253
+ ])
2254
+ })
2255
+
1882
2256
  test('log:emitted limits a tick and resumes within the block', async () => {
1883
2257
  const db = TestApp.database()
1884
2258
  const subscription = await Core.createSubscription(db, {
@@ -1903,13 +2277,23 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1903
2277
  },
1904
2278
  eventTypes: ['log:emitted'],
1905
2279
  headBlockNumber: windowHead,
1906
- tidx: realTidx,
1907
2280
  })
1908
2281
 
1909
- expect((await tick()).dispatched).toBe(1)
2282
+ const first = await tick()
2283
+ expect(first.dispatched).toBe(1)
2284
+ expect(first.hasMore).toBe(true)
1910
2285
  expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 0])
1911
- expect((await tick()).dispatched).toBe(1)
2286
+ const second = await tick()
2287
+ expect(second.dispatched).toBe(1)
2288
+ expect(second.hasMore).toBe(true)
1912
2289
  expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
2290
+ const third = await tick()
2291
+ expect(third.dispatched).toBe(0)
2292
+ expect(third.hasMore).toBeUndefined()
2293
+ expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
2294
+ windowTo,
2295
+ endOfBlockIndex,
2296
+ ])
1913
2297
  expect(
1914
2298
  dispatched.map(({ envelope }) => (envelope.data as { logIndex: number }).logIndex),
1915
2299
  ).toEqual([0, 1])
@@ -1942,7 +2326,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1942
2326
  headBlockNumber: windowTo + 3,
1943
2327
  client,
1944
2328
  db,
1945
- tidx: realTidx,
1946
2329
  })
1947
2330
 
1948
2331
  expect(result.scanned).toMatchInlineSnapshot(`1`)
@@ -1954,7 +2337,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1954
2337
  "miner": "0x0000000000000000000000000000000000000000",
1955
2338
  "number": 1000002,
1956
2339
  "parentHash": "0xa406dff9c51cc2abbef3a811f0499e99a568bf781019f129ab45dca1df0e43e5",
1957
- "timestamp": "2026-01-14T18:38:03.000Z",
2340
+ "timestamp": "2026-01-14T18:38:03.685Z",
1958
2341
  "transactionCount": 52,
1959
2342
  }
1960
2343
  `)
@@ -1992,7 +2375,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1992
2375
  },
1993
2376
  eventTypes: ['block:created'],
1994
2377
  headBlockNumber: windowHead,
1995
- tidx: realTidx,
1996
2378
  })
1997
2379
 
1998
2380
  expect((await tick()).dispatched).toBe(1)
@@ -2035,6 +2417,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2035
2417
  await Core.setCursor(db, healthy.id, windowCursor)
2036
2418
 
2037
2419
  const dispatched: Webhooks.poll.Dispatchable[] = []
2420
+ const subscriptions: number[] = []
2038
2421
 
2039
2422
  await expect(
2040
2423
  Webhooks.poll({
@@ -2048,11 +2431,14 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2048
2431
  client,
2049
2432
  db,
2050
2433
  maxBlocks: 3,
2051
- tidx: realTidx,
2434
+ onSubscriptions(count) {
2435
+ subscriptions.push(count)
2436
+ },
2052
2437
  }),
2053
2438
  ).rejects.toThrow()
2054
2439
 
2055
2440
  expect(await Core.getCursor(db, unavailable.id)).toBe(cursor)
2441
+ expect(subscriptions).toEqual([2])
2056
2442
  expect(decodeCursor(await Core.getCursor(db, healthy.id))).toMatchInlineSnapshot(`
2057
2443
  [
2058
2444
  1000002,
@@ -2090,7 +2476,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2090
2476
  headBlockNumber: windowTo + 3,
2091
2477
  client,
2092
2478
  db,
2093
- tidx: realTidx,
2094
2479
  })
2095
2480
 
2096
2481
  expect(result.scanned).toMatchInlineSnapshot(`1`)
@@ -2112,6 +2497,28 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2112
2497
  ])
2113
2498
  })
2114
2499
 
2500
+ test('disables stored subscriptions with removed receipt filters', async () => {
2501
+ const db = TestApp.database()
2502
+ const subscription = await Core.createSubscription(db, {
2503
+ chainId: 42431,
2504
+ destination: { type: 'url', url: 'https://hooks.example.com/legacy' },
2505
+ eventType: 'transaction:included',
2506
+ filters: { includeReceipt: true },
2507
+ owner: { orgId: 'org_legacy', type: 'api_key' },
2508
+ })
2509
+
2510
+ await Webhooks.poll({
2511
+ chainId: 42431,
2512
+ client,
2513
+ db,
2514
+ eventTypes: ['transaction:included'],
2515
+ headBlockNumber: windowHead,
2516
+ })
2517
+
2518
+ const disabled = await Core.getSubscription(db, subscription.owner, subscription.id)
2519
+ expect(disabled?.status).toBe('disabled')
2520
+ })
2521
+
2115
2522
  test('transaction:included limits a tick and resumes within the block', async () => {
2116
2523
  const db = TestApp.database()
2117
2524
  const subscription = await Core.createSubscription(db, {
@@ -2136,7 +2543,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2136
2543
  },
2137
2544
  eventTypes: ['transaction:included'],
2138
2545
  headBlockNumber: windowHead,
2139
- tidx: realTidx,
2140
2546
  })
2141
2547
 
2142
2548
  expect((await tick()).dispatched).toBe(1)
@@ -2168,7 +2574,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2168
2574
  headBlockNumber: missingBlock + 2,
2169
2575
  client,
2170
2576
  db,
2171
- tidx: realTidx,
2172
2577
  }),
2173
2578
  ).rejects.toThrowErrorMatchingInlineSnapshot(`
2174
2579
  [Webhooks.BlockUnavailableError: Block 4000000000 is unavailable.]
@@ -2199,7 +2604,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2199
2604
  headBlockNumber: windowHead,
2200
2605
  client,
2201
2606
  db,
2202
- tidx: realTidx,
2203
2607
  })
2204
2608
 
2205
2609
  expect(result.scanned).toMatchInlineSnapshot(`1`)
@@ -2209,78 +2613,10 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2209
2613
  })
2210
2614
  })
2211
2615
 
2212
- test('transaction:included routes a status filter through receipts', async () => {
2213
- const db = TestApp.database()
2214
- const subscription = await Core.createSubscription(db, {
2215
- chainId: 42431,
2216
- eventType: 'transaction:included',
2217
- filters: { from: windowSender, status: 'success' },
2218
- owner: { orgId: 'org_test', type: 'api_key' },
2219
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2220
- })
2221
- await Core.setCursor(db, subscription.id, windowCursor)
2222
-
2223
- const result = await Webhooks.poll({
2224
- chainId: 42431,
2225
- dispatch: async () => ({ ok: true }),
2226
- eventTypes: ['transaction:included'],
2227
- headBlockNumber: windowHead,
2228
- client,
2229
- db,
2230
- tidx: realTidx,
2231
- })
2232
-
2233
- // The window tx succeeded, so the status=success subquery keeps it.
2234
- expect(result.scanned).toMatchInlineSnapshot(`1`)
2235
- })
2236
-
2237
- test('transaction:included embeds the receipt when includeReceipt is set', async () => {
2238
- const db = TestApp.database()
2239
- const subscription = await Core.createSubscription(db, {
2240
- chainId: 42431,
2241
- eventType: 'transaction:included',
2242
- // No receipt-touching filter — `includeReceipt` alone must trigger the
2243
- // fetch and embed it in the delivered payload.
2244
- filters: { from: windowSender, input: { selector: '0xa9059cbb' }, includeReceipt: true },
2245
- owner: { orgId: 'org_test', type: 'api_key' },
2246
- destination: { type: 'url', url: 'https://hooks.example.com/x' },
2247
- })
2248
- await Core.setCursor(db, subscription.id, windowCursor)
2249
-
2250
- const dispatched: Webhooks.poll.Dispatchable[] = []
2251
- const result = await Webhooks.poll({
2252
- chainId: 42431,
2253
- dispatch: async (input) => {
2254
- dispatched.push(input)
2255
- return { ok: true }
2256
- },
2257
- eventTypes: ['transaction:included'],
2258
- headBlockNumber: windowHead,
2259
- client,
2260
- db,
2261
- tidx: realTidx,
2262
- })
2263
-
2264
- expect(result.scanned).toMatchInlineSnapshot(`1`)
2265
- const data = dispatched[0]?.envelope.data as TransactionEvent | undefined
2266
- // The receipt is embedded under `meta.receipt`, matching the read endpoint's
2267
- // `include=receipt` shape, and refers to the same transaction.
2268
- expect(data?.meta?.receipt?.status).toMatchInlineSnapshot(`"success"`)
2269
- expect(data?.meta?.receipt?.transactionHash).toBe(data?.hash)
2270
- const receipt = data?.meta.receipt
2271
- if (receipt?.meta.rpc.feeToken) {
2272
- expect(receipt.feeToken?.address).toBe(receipt.meta.rpc.feeToken)
2273
- expect(typeof receipt.feeToken?.currency).toBe('string')
2274
- expect(typeof receipt.feeToken?.decimals).toBe('number')
2275
- expect(typeof receipt.feeToken?.name).toBe('string')
2276
- expect(typeof receipt.feeToken?.symbol).toBe('string')
2277
- } else expect(receipt?.feeToken).toBeUndefined()
2278
- })
2279
-
2280
2616
  // End-to-end through the *default* transport: `poll` with no injected
2281
2617
  // `dispatch` falls back to `deliverAndRecord`, which signs and POSTs via
2282
- // `globalThis.fetch` and records the outcome. These stub the global fetch so a
2283
- // seeded TIDX row reaches a captured local sink with no network.
2618
+ // `globalThis.fetch` and records the outcome. These intercept the delivery
2619
+ // request while real RPC reads pass through.
2284
2620
  describe('default transport (deliverAndRecord)', () => {
2285
2621
  afterEach(() => {
2286
2622
  vi.restoreAllMocks()
@@ -2291,9 +2627,8 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2291
2627
  const subscription = await createTransferSubscription(db)
2292
2628
  await Core.setCursor(db, subscription.id, windowCursor)
2293
2629
 
2294
- // Real TIDX queries also go through `globalThis.fetch`, so the mock passes
2295
- // non-webhook requests through to the real network and only captures the
2296
- // delivery POST to the subscription URL.
2630
+ // RPC queries also go through `globalThis.fetch`, so the interceptor passes
2631
+ // non-webhook requests through and captures only the delivery POST.
2297
2632
  const realFetch = globalThis.fetch.bind(globalThis)
2298
2633
  const calls: { body: string; headers: Headers; url: string }[] = []
2299
2634
  vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
@@ -2317,7 +2652,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2317
2652
  headBlockNumber: windowHead,
2318
2653
  client,
2319
2654
  db,
2320
- tidx: realTidx,
2321
2655
  })
2322
2656
 
2323
2657
  expect(result.dispatched).toBe(1)
@@ -2369,7 +2703,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2369
2703
  headBlockNumber: windowHead,
2370
2704
  client,
2371
2705
  db,
2372
- tidx: realTidx,
2373
2706
  })
2374
2707
 
2375
2708
  const first = await tick()
@@ -2393,7 +2726,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2393
2726
  const subscription = await createTransferSubscription(db)
2394
2727
  await Core.setCursor(db, subscription.id, windowCursor)
2395
2728
 
2396
- // Real TIDX queries pass through; only the webhook delivery fails (500).
2729
+ // Real RPC queries pass through; only the webhook delivery fails (500).
2397
2730
  const realFetch = globalThis.fetch.bind(globalThis)
2398
2731
  vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
2399
2732
  const url =
@@ -2410,7 +2743,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2410
2743
  headBlockNumber: windowHead,
2411
2744
  client,
2412
2745
  db,
2413
- tidx: realTidx,
2414
2746
  })
2415
2747
 
2416
2748
  // Each tick delivers once and fails; after `maxFailures` ticks the
@@ -2428,7 +2760,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2428
2760
  })
2429
2761
 
2430
2762
  // Full loop through the public HTTP surface: create over the route, let the
2431
- // poller detect a seeded TIDX row and deliver via the default transport, then
2763
+ // poller detect an RPC transfer log and deliver via the default transport, then
2432
2764
  // read the recorded outcome back through the deliveries route. Unlike the
2433
2765
  // `poll` tests above (which seed subscriptions via the core helper), this
2434
2766
  // proves the route-issued secret and id line up with what is delivered/logged.
@@ -2467,7 +2799,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2467
2799
  return new Response(null, { status: 200 })
2468
2800
  }) satisfies typeof globalThis.fetch
2469
2801
 
2470
- // 3. Poll a real TIDX transfer row; dispatch through the real
2802
+ // 3. Poll a real RPC transfer log; dispatch through the real
2471
2803
  // sign+deliver+record core with the injected transport. `batchSize: 1`
2472
2804
  // delivers exactly one envelope from the window.
2473
2805
  const result = await Webhooks.poll({
@@ -2479,7 +2811,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2479
2811
  headBlockNumber: windowHead,
2480
2812
  client,
2481
2813
  db,
2482
- tidx: realTidx,
2483
2814
  })
2484
2815
  expect(result.dispatched).toBe(1)
2485
2816