tapimo 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/dist/App.d.ts +7410 -7613
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +0 -1
  4. package/dist/App.js.map +1 -1
  5. package/dist/Webhooks.d.ts +21 -20
  6. package/dist/Webhooks.d.ts.map +1 -1
  7. package/dist/Webhooks.js +66 -23
  8. package/dist/Webhooks.js.map +1 -1
  9. package/dist/admin/apps/verified-tokens.d.ts +74 -74
  10. package/dist/apps/Zones.d.ts +34 -22
  11. package/dist/apps/Zones.d.ts.map +1 -1
  12. package/dist/apps/Zones.js +9 -8
  13. package/dist/apps/Zones.js.map +1 -1
  14. package/dist/apps/data/App.d.ts +3120 -2782
  15. package/dist/apps/data/App.d.ts.map +1 -1
  16. package/dist/apps/data/FxOracle.d.ts +4 -5
  17. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  18. package/dist/apps/data/FxOracle.js +19 -4
  19. package/dist/apps/data/FxOracle.js.map +1 -1
  20. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  21. package/dist/apps/data/routes/activities.js +32 -27
  22. package/dist/apps/data/routes/activities.js.map +1 -1
  23. package/dist/apps/data/routes/addresses.d.ts +283 -52
  24. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  25. package/dist/apps/data/routes/balances.d.ts +291 -21
  26. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  27. package/dist/apps/data/routes/balances.js +232 -66
  28. package/dist/apps/data/routes/balances.js.map +1 -1
  29. package/dist/apps/data/routes/blocks.d.ts +4 -4
  30. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  31. package/dist/apps/data/routes/earn.d.ts +642 -126
  32. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  33. package/dist/apps/data/routes/earn.js +627 -58
  34. package/dist/apps/data/routes/earn.js.map +1 -1
  35. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  36. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  37. package/dist/apps/data/routes/exchanges.js +114 -118
  38. package/dist/apps/data/routes/exchanges.js.map +1 -1
  39. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  40. package/dist/apps/data/routes/receipts.js +68 -47
  41. package/dist/apps/data/routes/receipts.js.map +1 -1
  42. package/dist/apps/data/routes/tokens.d.ts +2 -0
  43. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  44. package/dist/apps/data/routes/transactions.d.ts +59 -51
  45. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  46. package/dist/apps/data/routes/transactions.js +25 -23
  47. package/dist/apps/data/routes/transactions.js.map +1 -1
  48. package/dist/apps/data/routes/transfers.d.ts +40 -18
  49. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  50. package/dist/apps/data/routes/transfers.js +618 -105
  51. package/dist/apps/data/routes/transfers.js.map +1 -1
  52. package/dist/apps/data/routes/valuation.d.ts +21 -21
  53. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  54. package/dist/apps/data/routes/valuation.js +2 -2
  55. package/dist/apps/data/routes/valuation.js.map +1 -1
  56. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  57. package/dist/apps/data/routes/webhooks.d.ts +102 -505
  58. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  59. package/dist/apps/data/routes/webhooks.js +484 -342
  60. package/dist/apps/data/routes/webhooks.js.map +1 -1
  61. package/dist/apps/data/routes/zones.d.ts +12 -12
  62. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  63. package/dist/apps/data/routes/zones.js +3 -1
  64. package/dist/apps/data/routes/zones.js.map +1 -1
  65. package/dist/apps/funding/App.d.ts +127 -386
  66. package/dist/apps/funding/App.d.ts.map +1 -1
  67. package/dist/apps/funding/App.js +20 -5
  68. package/dist/apps/funding/App.js.map +1 -1
  69. package/dist/apps/funding/routes/deposit-addresses.d.ts +141 -532
  70. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  71. package/dist/apps/funding/routes/deposit-addresses.js +21 -103
  72. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  73. package/dist/apps/funding/routes/transfers.d.ts +4 -4
  74. package/dist/apps/funding/routes/transfers.js +5 -5
  75. package/dist/apps/funding/routes/transfers.js.map +1 -1
  76. package/dist/apps/management/App.d.ts +429 -727
  77. package/dist/apps/management/App.d.ts.map +1 -1
  78. package/dist/apps/management/routes/api-keys.d.ts +24 -24
  79. package/dist/apps/management/routes/billing.d.ts +72 -72
  80. package/dist/apps/management/routes/faucet.d.ts +6 -6
  81. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  82. package/dist/apps/management/routes/me.d.ts +6 -6
  83. package/dist/apps/management/routes/members.d.ts +30 -30
  84. package/dist/apps/management/routes/projects.d.ts +20 -20
  85. package/dist/apps/management/routes/usage.d.ts +24 -24
  86. package/dist/apps/management/routes/webhooks.d.ts +158 -573
  87. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/mpp/App.d.ts +12 -0
  89. package/dist/apps/mpp/App.d.ts.map +1 -1
  90. package/dist/apps/mpp/Mpp.d.ts +3 -0
  91. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  92. package/dist/apps/mpp/Mpp.js +3 -0
  93. package/dist/apps/mpp/Mpp.js.map +1 -1
  94. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  95. package/dist/apps/mpp/internal/Adapter.js +9 -2
  96. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  97. package/dist/db/Db.d.ts +2 -2
  98. package/dist/db/Db.d.ts.map +1 -1
  99. package/dist/db/Db.js +25 -15
  100. package/dist/db/Db.js.map +1 -1
  101. package/dist/db/tables/fundingDeposits.d.ts +18 -1
  102. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  103. package/dist/db/tables/fundingDeposits.js +46 -2
  104. package/dist/db/tables/fundingDeposits.js.map +1 -1
  105. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  106. package/dist/db/tables/sponsoredTransactions.js +4 -1
  107. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  108. package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
  109. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  110. package/dist/db/tables/webhookSubscriptions.js +76 -3
  111. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  112. package/dist/internal/Auth.d.ts +4 -0
  113. package/dist/internal/Auth.d.ts.map +1 -1
  114. package/dist/internal/Auth.js +11 -2
  115. package/dist/internal/Auth.js.map +1 -1
  116. package/dist/internal/MetricSink.d.ts +112 -3
  117. package/dist/internal/MetricSink.d.ts.map +1 -1
  118. package/dist/internal/MetricSink.js +116 -3
  119. package/dist/internal/MetricSink.js.map +1 -1
  120. package/dist/internal/Schema.js +1 -1
  121. package/dist/internal/Schema.js.map +1 -1
  122. package/dist/internal/Tidx.d.ts +1 -3
  123. package/dist/internal/Tidx.d.ts.map +1 -1
  124. package/dist/internal/Tidx.js +1 -3
  125. package/dist/internal/Tidx.js.map +1 -1
  126. package/dist/internal/Viem.d.ts.map +1 -1
  127. package/dist/internal/Viem.js +2 -1
  128. package/dist/internal/Viem.js.map +1 -1
  129. package/dist/internal/WebhookDestination.d.ts +2 -0
  130. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  131. package/dist/internal/WebhookDestination.js +11 -0
  132. package/dist/internal/WebhookDestination.js.map +1 -1
  133. package/dist/internal/WebhookTransfer.d.ts +55 -0
  134. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  135. package/dist/internal/WebhookTransfer.js +296 -0
  136. package/dist/internal/WebhookTransfer.js.map +1 -0
  137. package/dist/internal/Webhooks.d.ts +37 -0
  138. package/dist/internal/Webhooks.d.ts.map +1 -1
  139. package/dist/internal/Webhooks.js +87 -13
  140. package/dist/internal/Webhooks.js.map +1 -1
  141. package/dist/internal/funding/Provider.d.ts +10 -6
  142. package/dist/internal/funding/Provider.d.ts.map +1 -1
  143. package/dist/internal/funding/Provider.js +11 -7
  144. package/dist/internal/funding/Provider.js.map +1 -1
  145. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  146. package/dist/internal/funding/Reconciliation.js +229 -16
  147. package/dist/internal/funding/Reconciliation.js.map +1 -1
  148. package/dist/internal/funding/providers/relay.d.ts +3 -0
  149. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  150. package/dist/internal/funding/providers/relay.js +18 -3
  151. package/dist/internal/funding/providers/relay.js.map +1 -1
  152. package/package.json +3 -3
  153. package/src/App.test.ts +32 -0
  154. package/src/App.ts +4 -1
  155. package/src/Client.test-d.ts +116 -38
  156. package/src/Webhooks.bench.ts +714 -0
  157. package/src/Webhooks.memory.bench.ts +250 -0
  158. package/src/Webhooks.test.ts +77 -6
  159. package/src/Webhooks.ts +83 -29
  160. package/src/apps/Zones.test.ts +5 -4
  161. package/src/apps/Zones.ts +9 -8
  162. package/src/apps/composition.test.ts +39 -10
  163. package/src/apps/data/FxOracle.test.ts +18 -0
  164. package/src/apps/data/FxOracle.ts +28 -5
  165. package/src/apps/data/routes/activities.test.ts +19 -0
  166. package/src/apps/data/routes/activities.ts +34 -30
  167. package/src/apps/data/routes/balances.test.ts +161 -10
  168. package/src/apps/data/routes/balances.ts +339 -147
  169. package/src/apps/data/routes/blocks.test.ts +12 -6
  170. package/src/apps/data/routes/earn.test.ts +286 -9
  171. package/src/apps/data/routes/earn.ts +841 -58
  172. package/src/apps/data/routes/exchanges.test.ts +7 -6
  173. package/src/apps/data/routes/exchanges.ts +124 -117
  174. package/src/apps/data/routes/receipts.test.ts +24 -7
  175. package/src/apps/data/routes/receipts.ts +67 -50
  176. package/src/apps/data/routes/tokens.test.ts +51 -0
  177. package/src/apps/data/routes/transactions.test.ts +42 -0
  178. package/src/apps/data/routes/transactions.ts +44 -39
  179. package/src/apps/data/routes/transfers.test.ts +402 -24
  180. package/src/apps/data/routes/transfers.ts +708 -101
  181. package/src/apps/data/routes/valuation.test.ts +26 -0
  182. package/src/apps/data/routes/valuation.ts +6 -2
  183. package/src/apps/data/routes/webhooks.test.ts +414 -130
  184. package/src/apps/data/routes/webhooks.ts +603 -419
  185. package/src/apps/data/routes/zones.ts +6 -1
  186. package/src/apps/funding/App.ts +20 -5
  187. package/src/apps/funding/routes/deposit-addresses.test.ts +43 -22
  188. package/src/apps/funding/routes/deposit-addresses.ts +34 -117
  189. package/src/apps/funding/routes/quotes.test.ts +1 -1
  190. package/src/apps/funding/routes/transfers.test.ts +6 -25
  191. package/src/apps/funding/routes/transfers.ts +5 -5
  192. package/src/apps/mpp/Mpp.test.ts +13 -1
  193. package/src/apps/mpp/Mpp.ts +8 -0
  194. package/src/apps/mpp/internal/Adapter.ts +10 -2
  195. package/src/db/Db.test.ts +40 -6
  196. package/src/db/Db.ts +42 -19
  197. package/src/db/tables/fundingDeposits.test.ts +101 -0
  198. package/src/db/tables/fundingDeposits.ts +71 -2
  199. package/src/db/tables/sponsoredTransactions.ts +9 -2
  200. package/src/db/tables/webhookSubscriptions.ts +116 -4
  201. package/src/internal/Auth.test.ts +28 -0
  202. package/src/internal/Auth.ts +15 -1
  203. package/src/internal/EarnVaults.test.ts +2 -2
  204. package/src/internal/MetricSink.test.ts +518 -0
  205. package/src/internal/MetricSink.ts +266 -9
  206. package/src/internal/Schema.ts +1 -1
  207. package/src/internal/Tidx.test.ts +4 -4
  208. package/src/internal/Tidx.ts +1 -3
  209. package/src/internal/Viem.test.ts +8 -0
  210. package/src/internal/Viem.ts +2 -1
  211. package/src/internal/WebhookDestination.test.ts +20 -0
  212. package/src/internal/WebhookDestination.ts +10 -0
  213. package/src/internal/WebhookTransfer.ts +426 -0
  214. package/src/internal/Webhooks.test.ts +137 -0
  215. package/src/internal/Webhooks.ts +131 -17
  216. package/src/internal/funding/Provider.test.ts +47 -7
  217. package/src/internal/funding/Provider.ts +16 -11
  218. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  219. package/src/internal/funding/Reconciliation.test.ts +1532 -54
  220. package/src/internal/funding/Reconciliation.ts +296 -53
  221. package/src/internal/funding/SourceObservation.test.ts +3 -2
  222. package/src/internal/funding/providers/relay.test.ts +1 -4
  223. package/src/internal/funding/providers/relay.ts +23 -3
@@ -2,7 +2,6 @@ import { type Db, 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",
@@ -1480,6 +1540,174 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1480
1540
  `)
1481
1541
  })
1482
1542
 
1543
+ test('scans event types concurrently without increasing scan bounds', async () => {
1544
+ const db = TestApp.database()
1545
+ const transfer = await createTransferSubscription(db)
1546
+ const block = await Core.createSubscription(db, {
1547
+ chainId: 42431,
1548
+ destination: { type: 'url', url: 'https://hooks.example.com/block' },
1549
+ eventType: 'block:created',
1550
+ filters: {},
1551
+ owner: { orgId: 'org_block', type: 'api_key' },
1552
+ })
1553
+ await Promise.all([
1554
+ Core.setCursor(db, transfer.id, windowCursor),
1555
+ Core.setCursor(db, block.id, windowCursor),
1556
+ ])
1557
+
1558
+ const active = new Map<
1559
+ Webhooks.EventType,
1560
+ Pick<Webhooks.poll.ScanCheckpoint, 'concurrency' | 'subscriptionLimit'>
1561
+ >()
1562
+ let maxConcurrency = 0
1563
+ let maxEventTypes = 0
1564
+ let maxSubscriptionLimit = 0
1565
+ await Webhooks.poll({
1566
+ chainId: 42431,
1567
+ client,
1568
+ db,
1569
+ dispatch: async () => ({ ok: true }),
1570
+ eventTypes: ['token:transfer', 'block:created'],
1571
+ headBlockNumber: windowHead,
1572
+ onScanCheckpoint(checkpoint) {
1573
+ expect(checkpoint.subscriptionCount).toBeLessThanOrEqual(checkpoint.subscriptionLimit)
1574
+ if (checkpoint.phase === 'start')
1575
+ active.set(checkpoint.eventType, {
1576
+ concurrency: checkpoint.concurrency,
1577
+ subscriptionLimit: checkpoint.subscriptionLimit,
1578
+ })
1579
+ else active.delete(checkpoint.eventType)
1580
+ maxConcurrency = Math.max(
1581
+ maxConcurrency,
1582
+ ...[...active.values()].map((value) => value.concurrency),
1583
+ )
1584
+ maxEventTypes = Math.max(maxEventTypes, active.size)
1585
+ maxSubscriptionLimit = Math.max(
1586
+ maxSubscriptionLimit,
1587
+ [...active.values()].reduce((total, value) => total + value.subscriptionLimit, 0),
1588
+ )
1589
+ },
1590
+ })
1591
+
1592
+ expect(active.size).toBe(0)
1593
+ expect(maxConcurrency).toBe(8)
1594
+ expect(maxEventTypes).toBe(2)
1595
+ expect(maxSubscriptionLimit).toBe(256)
1596
+ })
1597
+
1598
+ test('dispatches a page as one batch and advances the cursor', async () => {
1599
+ const db = TestApp.database()
1600
+ const subscription = await createTransferSubscription(db)
1601
+ await Core.setCursor(db, subscription.id, windowCursor)
1602
+
1603
+ const batches: Webhooks.poll.Dispatchable[][] = []
1604
+ const result = await Webhooks.poll({
1605
+ chainId: 42431,
1606
+ client,
1607
+ db,
1608
+ dispatchBatch: async (dispatchables) => {
1609
+ batches.push([...dispatchables])
1610
+ return { ok: true }
1611
+ },
1612
+ eventTypes: ['token:transfer'],
1613
+ headBlockNumber: windowHead,
1614
+ })
1615
+
1616
+ expect(result.dispatched).toBe(2)
1617
+ expect(batches.map((batch) => batch.length)).toEqual([2])
1618
+ expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
1619
+ })
1620
+
1621
+ test('keeps the cursor unchanged when a batch dispatch fails', async () => {
1622
+ const db = TestApp.database()
1623
+ const subscription = await createTransferSubscription(db)
1624
+ await Core.setCursor(db, subscription.id, windowCursor)
1625
+
1626
+ const result = await Webhooks.poll({
1627
+ chainId: 42431,
1628
+ client,
1629
+ db,
1630
+ dispatchBatch: async () => ({ error: 'unreachable', ok: false }),
1631
+ eventTypes: ['token:transfer'],
1632
+ headBlockNumber: windowHead,
1633
+ })
1634
+
1635
+ expect(result.dispatched).toBe(0)
1636
+ expect(result.failed).toBe(2)
1637
+ expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
1638
+ windowFrom - 1,
1639
+ endOfBlockIndex,
1640
+ ])
1641
+ })
1642
+
1643
+ test('matches the either-side address filter through Viem log filters', async () => {
1644
+ const db = TestApp.database()
1645
+ const subscription = await Core.createSubscription(db, {
1646
+ chainId: 42431,
1647
+ destination: { type: 'url', url: 'https://hooks.example.com/address' },
1648
+ eventType: 'token:transfer',
1649
+ filters: { address: windowSender },
1650
+ owner: { orgId: 'org_address', type: 'api_key' },
1651
+ })
1652
+ await Core.setCursor(db, subscription.id, windowCursor)
1653
+
1654
+ const positions: string[] = []
1655
+ const result = await Webhooks.poll({
1656
+ chainId: 42431,
1657
+ client,
1658
+ db,
1659
+ dispatch: async ({ envelope }) => {
1660
+ const data = envelope.data as { blockNumber: number; logIndex: number }
1661
+ positions.push(`${data.blockNumber}:${data.logIndex}`)
1662
+ return { ok: true }
1663
+ },
1664
+ eventTypes: ['token:transfer'],
1665
+ headBlockNumber: windowHead,
1666
+ })
1667
+
1668
+ expect(result.dispatched).toBe(2)
1669
+ expect(positions).toEqual(['1000002:0', '1000002:1'])
1670
+ })
1671
+
1672
+ test('keeps distinct transfer pushdowns isolated', async () => {
1673
+ const db = TestApp.database()
1674
+ const recipient = await Core.createSubscription(db, {
1675
+ chainId: 42431,
1676
+ destination: { type: 'url', url: 'https://hooks.example.com/recipient' },
1677
+ eventType: 'token:transfer',
1678
+ filters: { recipient: windowRecipient },
1679
+ owner: { orgId: 'org_recipient', type: 'api_key' },
1680
+ })
1681
+ const sender = await createTransferSubscription(db)
1682
+ await Promise.all([
1683
+ Core.setCursor(db, recipient.id, windowCursor),
1684
+ Core.setCursor(db, sender.id, windowCursor),
1685
+ ])
1686
+
1687
+ const dispatched: Webhooks.poll.Dispatchable[] = []
1688
+ const result = await Webhooks.poll({
1689
+ chainId: 42431,
1690
+ client,
1691
+ db,
1692
+ dispatch: async (input) => {
1693
+ dispatched.push(input)
1694
+ return { ok: true }
1695
+ },
1696
+ eventTypes: ['token:transfer'],
1697
+ headBlockNumber: windowHead,
1698
+ })
1699
+
1700
+ expect(result.dispatched).toBe(3)
1701
+ expect(
1702
+ Object.fromEntries(
1703
+ [...Map.groupBy(dispatched, ({ subscription }) => subscription.id)].map(([id, items]) => [
1704
+ id,
1705
+ items.length,
1706
+ ]),
1707
+ ),
1708
+ ).toEqual({ [recipient.id]: 1, [sender.id]: 2 })
1709
+ })
1710
+
1483
1711
  test('pauses the cursor at the last success when dispatch fails', async () => {
1484
1712
  const db = TestApp.database()
1485
1713
  const subscription = await createTransferSubscription(db)
@@ -1494,7 +1722,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1494
1722
  headBlockNumber: windowHead,
1495
1723
  client,
1496
1724
  db,
1497
- tidx: realTidx,
1498
1725
  })
1499
1726
 
1500
1727
  expect(result.dispatched).toMatchInlineSnapshot(`1`)
@@ -1522,7 +1749,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1522
1749
  headBlockNumber: windowHead,
1523
1750
  client,
1524
1751
  db,
1525
- tidx: realTidx,
1526
1752
  })
1527
1753
 
1528
1754
  expect(result.dispatched).toMatchInlineSnapshot(`0`)
@@ -1537,6 +1763,31 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1537
1763
  `)
1538
1764
  })
1539
1765
 
1766
+ test('stops dispatching when the poll signal is aborted', async () => {
1767
+ const db = TestApp.database()
1768
+ const subscription = await createTransferSubscription(db)
1769
+ await Core.setCursor(db, subscription.id, windowCursor)
1770
+
1771
+ const abort = new AbortController()
1772
+ let calls = 0
1773
+ await expect(
1774
+ Webhooks.poll({
1775
+ chainId: 42431,
1776
+ client,
1777
+ db,
1778
+ dispatch: async () => {
1779
+ calls++
1780
+ abort.abort(new Error('lease lost'))
1781
+ return { ok: true }
1782
+ },
1783
+ eventTypes: ['token:transfer'],
1784
+ headBlockNumber: windowHead,
1785
+ signal: abort.signal,
1786
+ }),
1787
+ ).rejects.toThrow('lease lost')
1788
+ expect(calls).toBe(1)
1789
+ })
1790
+
1540
1791
  test('drains two bounded transfer pages per tick without gaps', async () => {
1541
1792
  const db = TestApp.database()
1542
1793
  const subscription = await createVolumeSubscription(db)
@@ -1547,6 +1798,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1547
1798
  Webhooks.poll({
1548
1799
  chainId: 42431,
1549
1800
  client,
1801
+ confirmations: 2,
1550
1802
  db,
1551
1803
  dispatch: async ({ envelope }) => {
1552
1804
  const data = envelope.data as { blockNumber: number; logIndex: number }
@@ -1555,7 +1807,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1555
1807
  },
1556
1808
  eventTypes: ['token:transfer'],
1557
1809
  headBlockNumber: volumeHead,
1558
- tidx: realTidx,
1559
1810
  })
1560
1811
 
1561
1812
  expect((await tick()).dispatched).toBe(200)
@@ -1577,11 +1828,11 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1577
1828
  const first = await Webhooks.poll({
1578
1829
  chainId: 42431,
1579
1830
  client,
1831
+ confirmations: 2,
1580
1832
  db,
1581
1833
  dispatch: async () => (++calls === 101 ? { error: 'unreachable', ok: false } : { ok: true }),
1582
1834
  eventTypes: ['token:transfer'],
1583
1835
  headBlockNumber: volumeHead,
1584
- tidx: realTidx,
1585
1836
  })
1586
1837
 
1587
1838
  expect(first.dispatched).toBe(100)
@@ -1593,6 +1844,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1593
1844
  batchSize: 1,
1594
1845
  chainId: 42431,
1595
1846
  client,
1847
+ confirmations: 2,
1596
1848
  db,
1597
1849
  dispatch: async ({ envelope }) => {
1598
1850
  const data = envelope.data as { blockNumber: number; logIndex: number }
@@ -1601,7 +1853,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1601
1853
  },
1602
1854
  eventTypes: ['token:transfer'],
1603
1855
  headBlockNumber: volumeHead,
1604
- tidx: realTidx,
1605
1856
  })
1606
1857
  expect(resumed).toEqual([1_000_002, 10])
1607
1858
  })
@@ -1618,6 +1869,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1618
1869
  await Webhooks.poll({
1619
1870
  chainId: 42431,
1620
1871
  client,
1872
+ confirmations: 2,
1621
1873
  db,
1622
1874
  dispatch: async ({ subscription }) => {
1623
1875
  order.push(subscription.id)
@@ -1625,7 +1877,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1625
1877
  },
1626
1878
  eventTypes: ['token:transfer'],
1627
1879
  headBlockNumber: volumeHead,
1628
- tidx: realTidx,
1629
1880
  })
1630
1881
 
1631
1882
  expect(order).toHaveLength(400)
@@ -1651,7 +1902,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1651
1902
  client,
1652
1903
  db,
1653
1904
  // Rows exist in history, but a brand-new subscription must not replay them.
1654
- tidx: realTidx,
1655
1905
  })
1656
1906
 
1657
1907
  // Nothing scanned or dispatched on the first tick; the cursor is anchored at
@@ -1689,7 +1939,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1689
1939
  headBlockNumber: creationHead + 3,
1690
1940
  client,
1691
1941
  db,
1692
- tidx: realTidx,
1693
1942
  })
1694
1943
 
1695
1944
  expect(result.dispatched).toBe(2)
@@ -1729,15 +1978,14 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1729
1978
  headBlockNumber: windowHead,
1730
1979
  client,
1731
1980
  db,
1732
- tidx: realTidx,
1733
1981
  })
1734
1982
 
1735
1983
  expect(decodeCursor(await Core.getCursor(db, transfer.id))).toEqual([
1736
- startBlockNumber - 2,
1984
+ startBlockNumber - 1,
1737
1985
  endOfBlockIndex,
1738
1986
  ])
1739
1987
  expect(decodeCursor(await Core.getCursor(db, block.id))).toEqual([
1740
- startBlockNumber - 2,
1988
+ startBlockNumber - 1,
1741
1989
  endOfBlockIndex,
1742
1990
  ])
1743
1991
  })
@@ -1754,10 +2002,9 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1754
2002
  chainId: 42431,
1755
2003
  dispatch: async () => ({ ok: true }),
1756
2004
  eventTypes: ['token:transfer'],
1757
- headBlockNumber: windowTo + 4,
2005
+ headBlockNumber: windowTo + 3,
1758
2006
  client,
1759
2007
  db,
1760
- tidx: realTidx,
1761
2008
  })
1762
2009
 
1763
2010
  expect(result.scanned).toMatchInlineSnapshot(`0`)
@@ -1779,6 +2026,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1779
2026
  const transferSignature =
1780
2027
  'event Transfer(address indexed from, address indexed to, uint256 value)'
1781
2028
  const transferSelector = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
2029
+ const windowToken = '0x20c00000000000000000000071de0cd31ab0d105'
1782
2030
  const windowSenderTopic = `0x${'0'.repeat(24)}${windowSender.slice(2)}`
1783
2031
 
1784
2032
  test('log:emitted decodes args from a signature-matched event view', async () => {
@@ -1803,7 +2051,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1803
2051
  headBlockNumber: windowHead,
1804
2052
  client,
1805
2053
  db,
1806
- tidx: realTidx,
1807
2054
  })
1808
2055
 
1809
2056
  expect(result.scanned).toMatchInlineSnapshot(`2`)
@@ -1857,7 +2104,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1857
2104
  headBlockNumber: windowHead,
1858
2105
  client,
1859
2106
  db,
1860
- tidx: realTidx,
1861
2107
  })
1862
2108
 
1863
2109
  expect(result.scanned).toMatchInlineSnapshot(`2`)
@@ -1879,6 +2125,87 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1879
2125
  `)
1880
2126
  })
1881
2127
 
2128
+ test('log:emitted scans an address-only filter', async () => {
2129
+ const db = TestApp.database()
2130
+ const subscription = await Core.createSubscription(db, {
2131
+ chainId: 42431,
2132
+ destination: { type: 'url', url: 'https://hooks.example.com/address-only' },
2133
+ eventType: 'log:emitted',
2134
+ filters: { address: '0xffffffffffffffffffffffffffffffffffffffff' },
2135
+ owner: { orgId: 'org_address_only', type: 'api_key' },
2136
+ })
2137
+ await Core.setCursor(db, subscription.id, windowCursor)
2138
+
2139
+ const result = await Webhooks.poll({
2140
+ chainId: 42431,
2141
+ client,
2142
+ db,
2143
+ eventTypes: ['log:emitted'],
2144
+ headBlockNumber: windowHead,
2145
+ })
2146
+
2147
+ expect(result.scanned).toBe(0)
2148
+ expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
2149
+ windowTo,
2150
+ endOfBlockIndex,
2151
+ ])
2152
+ })
2153
+
2154
+ test('log:emitted keeps distinct address and topic pushdowns isolated', async () => {
2155
+ const db = TestApp.database()
2156
+ const address = await Core.createSubscription(db, {
2157
+ chainId: 42431,
2158
+ destination: { type: 'url', url: 'https://hooks.example.com/address' },
2159
+ eventType: 'log:emitted',
2160
+ filters: {
2161
+ address: windowToken,
2162
+ args: { from: windowSender },
2163
+ signature: transferSignature,
2164
+ },
2165
+ owner: { orgId: 'org_address', type: 'api_key' },
2166
+ })
2167
+ const topic = await Core.createSubscription(db, {
2168
+ chainId: 42431,
2169
+ destination: { type: 'url', url: 'https://hooks.example.com/topic' },
2170
+ eventType: 'log:emitted',
2171
+ filters: { topic0: transferSelector, topic1: windowSenderTopic },
2172
+ owner: { orgId: 'org_topic', type: 'api_key' },
2173
+ })
2174
+ await Promise.all([
2175
+ Core.setCursor(db, address.id, windowCursor),
2176
+ Core.setCursor(db, topic.id, windowCursor),
2177
+ ])
2178
+
2179
+ const dispatched: Webhooks.poll.Dispatchable[] = []
2180
+ const result = await Webhooks.poll({
2181
+ chainId: 42431,
2182
+ client,
2183
+ db,
2184
+ dispatch: async (input) => {
2185
+ dispatched.push(input)
2186
+ return { ok: true }
2187
+ },
2188
+ eventTypes: ['log:emitted'],
2189
+ headBlockNumber: windowHead,
2190
+ })
2191
+
2192
+ expect(result.dispatched).toBe(3)
2193
+ expect(
2194
+ Object.fromEntries(
2195
+ [...Map.groupBy(dispatched, ({ subscription }) => subscription.id)].map(([id, items]) => [
2196
+ id,
2197
+ items.length,
2198
+ ]),
2199
+ ),
2200
+ ).toEqual({ [address.id]: 1, [topic.id]: 2 })
2201
+ expect(
2202
+ await Promise.all([Core.getCursor(db, address.id), Core.getCursor(db, topic.id)]),
2203
+ ).toEqual([
2204
+ encodeCursor([windowTo, endOfBlockIndex]),
2205
+ encodeCursor([windowTo, endOfBlockIndex]),
2206
+ ])
2207
+ })
2208
+
1882
2209
  test('log:emitted limits a tick and resumes within the block', async () => {
1883
2210
  const db = TestApp.database()
1884
2211
  const subscription = await Core.createSubscription(db, {
@@ -1903,13 +2230,23 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1903
2230
  },
1904
2231
  eventTypes: ['log:emitted'],
1905
2232
  headBlockNumber: windowHead,
1906
- tidx: realTidx,
1907
2233
  })
1908
2234
 
1909
- expect((await tick()).dispatched).toBe(1)
2235
+ const first = await tick()
2236
+ expect(first.dispatched).toBe(1)
2237
+ expect(first.hasMore).toBe(true)
1910
2238
  expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 0])
1911
- expect((await tick()).dispatched).toBe(1)
2239
+ const second = await tick()
2240
+ expect(second.dispatched).toBe(1)
2241
+ expect(second.hasMore).toBe(true)
1912
2242
  expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([windowTo, 1])
2243
+ const third = await tick()
2244
+ expect(third.dispatched).toBe(0)
2245
+ expect(third.hasMore).toBeUndefined()
2246
+ expect(decodeCursor(await Core.getCursor(db, subscription.id))).toEqual([
2247
+ windowTo,
2248
+ endOfBlockIndex,
2249
+ ])
1913
2250
  expect(
1914
2251
  dispatched.map(({ envelope }) => (envelope.data as { logIndex: number }).logIndex),
1915
2252
  ).toEqual([0, 1])
@@ -1942,7 +2279,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1942
2279
  headBlockNumber: windowTo + 3,
1943
2280
  client,
1944
2281
  db,
1945
- tidx: realTidx,
1946
2282
  })
1947
2283
 
1948
2284
  expect(result.scanned).toMatchInlineSnapshot(`1`)
@@ -1992,7 +2328,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
1992
2328
  },
1993
2329
  eventTypes: ['block:created'],
1994
2330
  headBlockNumber: windowHead,
1995
- tidx: realTidx,
1996
2331
  })
1997
2332
 
1998
2333
  expect((await tick()).dispatched).toBe(1)
@@ -2035,6 +2370,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2035
2370
  await Core.setCursor(db, healthy.id, windowCursor)
2036
2371
 
2037
2372
  const dispatched: Webhooks.poll.Dispatchable[] = []
2373
+ const subscriptions: number[] = []
2038
2374
 
2039
2375
  await expect(
2040
2376
  Webhooks.poll({
@@ -2048,11 +2384,14 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2048
2384
  client,
2049
2385
  db,
2050
2386
  maxBlocks: 3,
2051
- tidx: realTidx,
2387
+ onSubscriptions(count) {
2388
+ subscriptions.push(count)
2389
+ },
2052
2390
  }),
2053
2391
  ).rejects.toThrow()
2054
2392
 
2055
2393
  expect(await Core.getCursor(db, unavailable.id)).toBe(cursor)
2394
+ expect(subscriptions).toEqual([2])
2056
2395
  expect(decodeCursor(await Core.getCursor(db, healthy.id))).toMatchInlineSnapshot(`
2057
2396
  [
2058
2397
  1000002,
@@ -2090,7 +2429,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2090
2429
  headBlockNumber: windowTo + 3,
2091
2430
  client,
2092
2431
  db,
2093
- tidx: realTidx,
2094
2432
  })
2095
2433
 
2096
2434
  expect(result.scanned).toMatchInlineSnapshot(`1`)
@@ -2112,6 +2450,28 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2112
2450
  ])
2113
2451
  })
2114
2452
 
2453
+ test('disables stored subscriptions with removed receipt filters', async () => {
2454
+ const db = TestApp.database()
2455
+ const subscription = await Core.createSubscription(db, {
2456
+ chainId: 42431,
2457
+ destination: { type: 'url', url: 'https://hooks.example.com/legacy' },
2458
+ eventType: 'transaction:included',
2459
+ filters: { includeReceipt: true },
2460
+ owner: { orgId: 'org_legacy', type: 'api_key' },
2461
+ })
2462
+
2463
+ await Webhooks.poll({
2464
+ chainId: 42431,
2465
+ client,
2466
+ db,
2467
+ eventTypes: ['transaction:included'],
2468
+ headBlockNumber: windowHead,
2469
+ })
2470
+
2471
+ const disabled = await Core.getSubscription(db, subscription.owner, subscription.id)
2472
+ expect(disabled?.status).toBe('disabled')
2473
+ })
2474
+
2115
2475
  test('transaction:included limits a tick and resumes within the block', async () => {
2116
2476
  const db = TestApp.database()
2117
2477
  const subscription = await Core.createSubscription(db, {
@@ -2136,7 +2496,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2136
2496
  },
2137
2497
  eventTypes: ['transaction:included'],
2138
2498
  headBlockNumber: windowHead,
2139
- tidx: realTidx,
2140
2499
  })
2141
2500
 
2142
2501
  expect((await tick()).dispatched).toBe(1)
@@ -2168,7 +2527,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2168
2527
  headBlockNumber: missingBlock + 2,
2169
2528
  client,
2170
2529
  db,
2171
- tidx: realTidx,
2172
2530
  }),
2173
2531
  ).rejects.toThrowErrorMatchingInlineSnapshot(`
2174
2532
  [Webhooks.BlockUnavailableError: Block 4000000000 is unavailable.]
@@ -2199,7 +2557,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2199
2557
  headBlockNumber: windowHead,
2200
2558
  client,
2201
2559
  db,
2202
- tidx: realTidx,
2203
2560
  })
2204
2561
 
2205
2562
  expect(result.scanned).toMatchInlineSnapshot(`1`)
@@ -2209,78 +2566,10 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2209
2566
  })
2210
2567
  })
2211
2568
 
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
2569
  // End-to-end through the *default* transport: `poll` with no injected
2281
2570
  // `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.
2571
+ // `globalThis.fetch` and records the outcome. These intercept the delivery
2572
+ // request while real RPC reads pass through.
2284
2573
  describe('default transport (deliverAndRecord)', () => {
2285
2574
  afterEach(() => {
2286
2575
  vi.restoreAllMocks()
@@ -2291,9 +2580,8 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2291
2580
  const subscription = await createTransferSubscription(db)
2292
2581
  await Core.setCursor(db, subscription.id, windowCursor)
2293
2582
 
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.
2583
+ // RPC queries also go through `globalThis.fetch`, so the interceptor passes
2584
+ // non-webhook requests through and captures only the delivery POST.
2297
2585
  const realFetch = globalThis.fetch.bind(globalThis)
2298
2586
  const calls: { body: string; headers: Headers; url: string }[] = []
2299
2587
  vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
@@ -2317,7 +2605,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2317
2605
  headBlockNumber: windowHead,
2318
2606
  client,
2319
2607
  db,
2320
- tidx: realTidx,
2321
2608
  })
2322
2609
 
2323
2610
  expect(result.dispatched).toBe(1)
@@ -2369,7 +2656,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2369
2656
  headBlockNumber: windowHead,
2370
2657
  client,
2371
2658
  db,
2372
- tidx: realTidx,
2373
2659
  })
2374
2660
 
2375
2661
  const first = await tick()
@@ -2393,7 +2679,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2393
2679
  const subscription = await createTransferSubscription(db)
2394
2680
  await Core.setCursor(db, subscription.id, windowCursor)
2395
2681
 
2396
- // Real TIDX queries pass through; only the webhook delivery fails (500).
2682
+ // Real RPC queries pass through; only the webhook delivery fails (500).
2397
2683
  const realFetch = globalThis.fetch.bind(globalThis)
2398
2684
  vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
2399
2685
  const url =
@@ -2410,7 +2696,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2410
2696
  headBlockNumber: windowHead,
2411
2697
  client,
2412
2698
  db,
2413
- tidx: realTidx,
2414
2699
  })
2415
2700
 
2416
2701
  // Each tick delivers once and fails; after `maxFailures` ticks the
@@ -2428,7 +2713,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2428
2713
  })
2429
2714
 
2430
2715
  // 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
2716
+ // poller detect an RPC transfer log and deliver via the default transport, then
2432
2717
  // read the recorded outcome back through the deliveries route. Unlike the
2433
2718
  // `poll` tests above (which seed subscriptions via the core helper), this
2434
2719
  // proves the route-issued secret and id line up with what is delivered/logged.
@@ -2467,7 +2752,7 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2467
2752
  return new Response(null, { status: 200 })
2468
2753
  }) satisfies typeof globalThis.fetch
2469
2754
 
2470
- // 3. Poll a real TIDX transfer row; dispatch through the real
2755
+ // 3. Poll a real RPC transfer log; dispatch through the real
2471
2756
  // sign+deliver+record core with the injected transport. `batchSize: 1`
2472
2757
  // delivers exactly one envelope from the window.
2473
2758
  const result = await Webhooks.poll({
@@ -2479,7 +2764,6 @@ describe.skipIf(Runtime.get().mode === 'localnet')('poll', () => {
2479
2764
  headBlockNumber: windowHead,
2480
2765
  client,
2481
2766
  db,
2482
- tidx: realTidx,
2483
2767
  })
2484
2768
  expect(result.dispatched).toBe(1)
2485
2769