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.
- package/dist/App.d.ts +7410 -7613
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +0 -1
- package/dist/App.js.map +1 -1
- package/dist/Webhooks.d.ts +21 -20
- package/dist/Webhooks.d.ts.map +1 -1
- package/dist/Webhooks.js +66 -23
- package/dist/Webhooks.js.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +74 -74
- package/dist/apps/Zones.d.ts +34 -22
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +9 -8
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +3120 -2782
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.d.ts +4 -5
- package/dist/apps/data/FxOracle.d.ts.map +1 -1
- package/dist/apps/data/FxOracle.js +19 -4
- package/dist/apps/data/FxOracle.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +32 -27
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +283 -52
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +291 -21
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +232 -66
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +4 -4
- package/dist/apps/data/routes/coingecko.d.ts +54 -54
- package/dist/apps/data/routes/earn.d.ts +642 -126
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +627 -58
- package/dist/apps/data/routes/earn.js.map +1 -1
- package/dist/apps/data/routes/exchanges.d.ts +120 -120
- package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
- package/dist/apps/data/routes/exchanges.js +114 -118
- package/dist/apps/data/routes/exchanges.js.map +1 -1
- package/dist/apps/data/routes/receipts.d.ts.map +1 -1
- package/dist/apps/data/routes/receipts.js +68 -47
- package/dist/apps/data/routes/receipts.js.map +1 -1
- package/dist/apps/data/routes/tokens.d.ts +2 -0
- package/dist/apps/data/routes/tokens.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.d.ts +59 -51
- package/dist/apps/data/routes/transactions.d.ts.map +1 -1
- package/dist/apps/data/routes/transactions.js +25 -23
- package/dist/apps/data/routes/transactions.js.map +1 -1
- package/dist/apps/data/routes/transfers.d.ts +40 -18
- package/dist/apps/data/routes/transfers.d.ts.map +1 -1
- package/dist/apps/data/routes/transfers.js +618 -105
- package/dist/apps/data/routes/transfers.js.map +1 -1
- package/dist/apps/data/routes/valuation.d.ts +21 -21
- package/dist/apps/data/routes/valuation.d.ts.map +1 -1
- package/dist/apps/data/routes/valuation.js +2 -2
- package/dist/apps/data/routes/valuation.js.map +1 -1
- package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
- package/dist/apps/data/routes/webhooks.d.ts +102 -505
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +484 -342
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +12 -12
- package/dist/apps/data/routes/zones.d.ts.map +1 -1
- package/dist/apps/data/routes/zones.js +3 -1
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/App.d.ts +127 -386
- package/dist/apps/funding/App.d.ts.map +1 -1
- package/dist/apps/funding/App.js +20 -5
- package/dist/apps/funding/App.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.d.ts +141 -532
- package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +21 -103
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/transfers.d.ts +4 -4
- package/dist/apps/funding/routes/transfers.js +5 -5
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +429 -727
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +24 -24
- package/dist/apps/management/routes/billing.d.ts +72 -72
- package/dist/apps/management/routes/faucet.d.ts +6 -6
- package/dist/apps/management/routes/invite-links.d.ts +38 -38
- package/dist/apps/management/routes/me.d.ts +6 -6
- package/dist/apps/management/routes/members.d.ts +30 -30
- package/dist/apps/management/routes/projects.d.ts +20 -20
- package/dist/apps/management/routes/usage.d.ts +24 -24
- package/dist/apps/management/routes/webhooks.d.ts +158 -573
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/mpp/App.d.ts +12 -0
- package/dist/apps/mpp/App.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.d.ts +3 -0
- package/dist/apps/mpp/Mpp.d.ts.map +1 -1
- package/dist/apps/mpp/Mpp.js +3 -0
- package/dist/apps/mpp/Mpp.js.map +1 -1
- package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
- package/dist/apps/mpp/internal/Adapter.js +9 -2
- package/dist/apps/mpp/internal/Adapter.js.map +1 -1
- package/dist/db/Db.d.ts +2 -2
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +25 -15
- package/dist/db/Db.js.map +1 -1
- package/dist/db/tables/fundingDeposits.d.ts +18 -1
- package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
- package/dist/db/tables/fundingDeposits.js +46 -2
- package/dist/db/tables/fundingDeposits.js.map +1 -1
- package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
- package/dist/db/tables/sponsoredTransactions.js +4 -1
- package/dist/db/tables/sponsoredTransactions.js.map +1 -1
- package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +76 -3
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +4 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +11 -2
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +112 -3
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +116 -3
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/Schema.js +1 -1
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Tidx.d.ts +1 -3
- package/dist/internal/Tidx.d.ts.map +1 -1
- package/dist/internal/Tidx.js +1 -3
- package/dist/internal/Tidx.js.map +1 -1
- package/dist/internal/Viem.d.ts.map +1 -1
- package/dist/internal/Viem.js +2 -1
- package/dist/internal/Viem.js.map +1 -1
- package/dist/internal/WebhookDestination.d.ts +2 -0
- package/dist/internal/WebhookDestination.d.ts.map +1 -1
- package/dist/internal/WebhookDestination.js +11 -0
- package/dist/internal/WebhookDestination.js.map +1 -1
- package/dist/internal/WebhookTransfer.d.ts +55 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -0
- package/dist/internal/WebhookTransfer.js +296 -0
- package/dist/internal/WebhookTransfer.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +37 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +87 -13
- package/dist/internal/Webhooks.js.map +1 -1
- package/dist/internal/funding/Provider.d.ts +10 -6
- package/dist/internal/funding/Provider.d.ts.map +1 -1
- package/dist/internal/funding/Provider.js +11 -7
- package/dist/internal/funding/Provider.js.map +1 -1
- package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
- package/dist/internal/funding/Reconciliation.js +229 -16
- package/dist/internal/funding/Reconciliation.js.map +1 -1
- package/dist/internal/funding/providers/relay.d.ts +3 -0
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +18 -3
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/App.test.ts +32 -0
- package/src/App.ts +4 -1
- package/src/Client.test-d.ts +116 -38
- package/src/Webhooks.bench.ts +714 -0
- package/src/Webhooks.memory.bench.ts +250 -0
- package/src/Webhooks.test.ts +77 -6
- package/src/Webhooks.ts +83 -29
- package/src/apps/Zones.test.ts +5 -4
- package/src/apps/Zones.ts +9 -8
- package/src/apps/composition.test.ts +39 -10
- package/src/apps/data/FxOracle.test.ts +18 -0
- package/src/apps/data/FxOracle.ts +28 -5
- package/src/apps/data/routes/activities.test.ts +19 -0
- package/src/apps/data/routes/activities.ts +34 -30
- package/src/apps/data/routes/balances.test.ts +161 -10
- package/src/apps/data/routes/balances.ts +339 -147
- package/src/apps/data/routes/blocks.test.ts +12 -6
- package/src/apps/data/routes/earn.test.ts +286 -9
- package/src/apps/data/routes/earn.ts +841 -58
- package/src/apps/data/routes/exchanges.test.ts +7 -6
- package/src/apps/data/routes/exchanges.ts +124 -117
- package/src/apps/data/routes/receipts.test.ts +24 -7
- package/src/apps/data/routes/receipts.ts +67 -50
- package/src/apps/data/routes/tokens.test.ts +51 -0
- package/src/apps/data/routes/transactions.test.ts +42 -0
- package/src/apps/data/routes/transactions.ts +44 -39
- package/src/apps/data/routes/transfers.test.ts +402 -24
- package/src/apps/data/routes/transfers.ts +708 -101
- package/src/apps/data/routes/valuation.test.ts +26 -0
- package/src/apps/data/routes/valuation.ts +6 -2
- package/src/apps/data/routes/webhooks.test.ts +414 -130
- package/src/apps/data/routes/webhooks.ts +603 -419
- package/src/apps/data/routes/zones.ts +6 -1
- package/src/apps/funding/App.ts +20 -5
- package/src/apps/funding/routes/deposit-addresses.test.ts +43 -22
- package/src/apps/funding/routes/deposit-addresses.ts +34 -117
- package/src/apps/funding/routes/quotes.test.ts +1 -1
- package/src/apps/funding/routes/transfers.test.ts +6 -25
- package/src/apps/funding/routes/transfers.ts +5 -5
- package/src/apps/mpp/Mpp.test.ts +13 -1
- package/src/apps/mpp/Mpp.ts +8 -0
- package/src/apps/mpp/internal/Adapter.ts +10 -2
- package/src/db/Db.test.ts +40 -6
- package/src/db/Db.ts +42 -19
- package/src/db/tables/fundingDeposits.test.ts +101 -0
- package/src/db/tables/fundingDeposits.ts +71 -2
- package/src/db/tables/sponsoredTransactions.ts +9 -2
- package/src/db/tables/webhookSubscriptions.ts +116 -4
- package/src/internal/Auth.test.ts +28 -0
- package/src/internal/Auth.ts +15 -1
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +518 -0
- package/src/internal/MetricSink.ts +266 -9
- package/src/internal/Schema.ts +1 -1
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Tidx.ts +1 -3
- package/src/internal/Viem.test.ts +8 -0
- package/src/internal/Viem.ts +2 -1
- package/src/internal/WebhookDestination.test.ts +20 -0
- package/src/internal/WebhookDestination.ts +10 -0
- package/src/internal/WebhookTransfer.ts +426 -0
- package/src/internal/Webhooks.test.ts +137 -0
- package/src/internal/Webhooks.ts +131 -17
- package/src/internal/funding/Provider.test.ts +47 -7
- package/src/internal/funding/Provider.ts +16 -11
- package/src/internal/funding/Reconciliation.local.test.ts +3 -1
- package/src/internal/funding/Reconciliation.test.ts +1532 -54
- package/src/internal/funding/Reconciliation.ts +296 -53
- package/src/internal/funding/SourceObservation.test.ts +3 -2
- package/src/internal/funding/providers/relay.test.ts +1 -4
- 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
|
|
1356
|
-
//
|
|
1357
|
-
//
|
|
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
|
-
//
|
|
1376
|
-
//
|
|
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 +
|
|
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
|
-
|
|
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
|
|
1456
|
-
//
|
|
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 -
|
|
1984
|
+
startBlockNumber - 1,
|
|
1737
1985
|
endOfBlockIndex,
|
|
1738
1986
|
])
|
|
1739
1987
|
expect(decodeCursor(await Core.getCursor(db, block.id))).toEqual([
|
|
1740
|
-
startBlockNumber -
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2283
|
-
//
|
|
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
|
-
//
|
|
2295
|
-
// non-webhook requests through
|
|
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
|
|
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
|
|
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
|
|
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
|
|