tapimo 0.9.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 +4887 -5074
- package/dist/App.d.ts.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 +168 -168
- 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 +5198 -5099
- 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 +270 -270
- 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 +52 -52
- package/dist/apps/data/routes/balances.d.ts +36 -36
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +135 -81
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +125 -125
- package/dist/apps/data/routes/coingecko.d.ts +54 -54
- package/dist/apps/data/routes/earn.d.ts +512 -58
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +368 -44
- 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/rpc.d.ts +31 -31
- package/dist/apps/data/routes/tokens.d.ts +461 -459
- 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 +108 -86
- 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 +96 -96
- package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
- package/dist/apps/funding/routes/quotes.d.ts +89 -89
- package/dist/apps/funding/routes/transfers.d.ts +4 -4
- 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/invitations.d.ts +134 -134
- 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/orgs.d.ts +98 -98
- 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/verified-token-requests.d.ts +83 -83
- 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 +56 -44
- 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 +17 -15
- package/dist/db/Db.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/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 +4 -0
- 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 +128 -0
- 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 +12 -1
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.test-d.ts +60 -5
- 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/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 +119 -10
- package/src/apps/data/routes/balances.ts +154 -95
- package/src/apps/data/routes/blocks.test.ts +12 -6
- package/src/apps/data/routes/earn.test.ts +197 -0
- package/src/apps/data/routes/earn.ts +486 -44
- 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/routes/quotes.test.ts +1 -1
- 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 +33 -19
- package/src/db/tables/sponsoredTransactions.ts +9 -2
- package/src/db/tables/webhookSubscriptions.ts +116 -4
- 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 +12 -7
- package/src/internal/funding/Reconciliation.local.test.ts +3 -1
- package/src/internal/funding/Reconciliation.test.ts +1240 -0
- package/src/internal/funding/Reconciliation.ts +136 -0
- package/src/internal/funding/SourceObservation.test.ts +1 -0
- package/src/internal/funding/providers/relay.test.ts +1 -4
- package/src/internal/funding/providers/relay.ts +16 -1
|
@@ -12,8 +12,8 @@ import * as Store from '../../../internal/Store.js';
|
|
|
12
12
|
import * as Ttl from '../../../internal/Ttl.js';
|
|
13
13
|
import * as Value from '../../../internal/Value.js';
|
|
14
14
|
import * as WebhookDestination from '../../../internal/WebhookDestination.js';
|
|
15
|
+
import * as WebhookTransfer from '../../../internal/WebhookTransfer.js';
|
|
15
16
|
import * as Webhooks from '../../../internal/Webhooks.js';
|
|
16
|
-
import * as Receipts from './receipts.js';
|
|
17
17
|
import * as Transactions from './transactions.js';
|
|
18
18
|
import * as Transfers from './transfers.js';
|
|
19
19
|
/** Default cap on live subscriptions per owner, overridable via `webhook.maxPerOwner`. */
|
|
@@ -52,7 +52,7 @@ const eventTypeDescriptions = [
|
|
|
52
52
|
type: 'token:transfer',
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
|
-
description: 'A transaction included in a block, filterable on
|
|
55
|
+
description: 'A transaction included in a block, filterable on transaction fields such as sender, recipient, value, calldata, inner account-abstraction calls, fees, and nonce.',
|
|
56
56
|
type: 'transaction:included',
|
|
57
57
|
},
|
|
58
58
|
];
|
|
@@ -173,13 +173,12 @@ export var schema;
|
|
|
173
173
|
schema.TransferFilters = Transfers.schema.Predicates.check(z.describe('Filters that narrow which TIP-20 transfer events trigger this webhook.'), z.meta({ title: 'Transfer filters' }));
|
|
174
174
|
/**
|
|
175
175
|
* `transaction:included` subscription filters: operators over every column the
|
|
176
|
-
* `txs` envelope exposes, plus inner account-abstraction `calls
|
|
177
|
-
* top-level `input` calldata
|
|
178
|
-
*
|
|
176
|
+
* `txs` envelope exposes, plus inner account-abstraction `calls` and the
|
|
177
|
+
* top-level `input` calldata. All optional and AND-combined; omit every field
|
|
178
|
+
* for the full stream.
|
|
179
179
|
*
|
|
180
180
|
* `address` matches either side (`from` OR `to`); when `includeCalls` is set
|
|
181
181
|
* (default), `to`/`address` also match an inner call target (`calls[].to`).
|
|
182
|
-
* Set `includeReceipt` to embed the receipt in the delivered payload.
|
|
183
182
|
*/
|
|
184
183
|
schema.TxFilters = z
|
|
185
184
|
.strictObject({
|
|
@@ -190,9 +189,6 @@ export var schema;
|
|
|
190
189
|
includeCalls: z.optional(z
|
|
191
190
|
.boolean()
|
|
192
191
|
.check(z.describe('When true (default), `to`/`address` also match an inner account-abstraction call target (`calls[].to`).'), z.meta({ examples: [true] }))),
|
|
193
|
-
includeReceipt: z.optional(z
|
|
194
|
-
.boolean()
|
|
195
|
-
.check(z.describe('When true, embed the transaction receipt (execution `status`, `gasUsed`, `logs`, …) in the delivered payload under `meta.receipt`. Default false (the receipt is an extra fetch). Mirrors the read endpoint\u2019s `include=receipt`.'), z.meta({ examples: [true] }))),
|
|
196
192
|
value: z.optional(schema.compare.check(z.describe('Match the transaction value (wei) bounds.'))),
|
|
197
193
|
input: z.optional(schema.bytes.check(z.describe('Match top-level calldata by 4-byte `selector`, hex `startsWith`, or exact.'))),
|
|
198
194
|
calls: z.optional(z
|
|
@@ -203,9 +199,7 @@ export var schema;
|
|
|
203
199
|
callCount: z.optional(schema.compare.check(z.describe('Match the number of inner calls (`call_count`).'))),
|
|
204
200
|
txType: z.optional(schema.eq(Schema.Hex).check(z.describe('Match the transaction type (e.g. `0x76` for Tempo AA).'))),
|
|
205
201
|
feeToken: z.optional(schema.eq(Schema.TokenAddress).check(z.describe('Match the fee token address.'))),
|
|
206
|
-
feePayer: z.optional(schema.eq(Schema.Address).check(z.describe('Match the fee payer address.'))),
|
|
207
202
|
gasLimit: z.optional(schema.compare.check(z.describe('Match the gas limit bounds.'))),
|
|
208
|
-
gasUsed: z.optional(schema.compare.check(z.describe('Match the gas used bounds.'))),
|
|
209
203
|
maxFeePerGas: z.optional(schema.compare.check(z.describe('Match the max fee per gas bounds.'))),
|
|
210
204
|
maxPriorityFeePerGas: z.optional(schema.compare.check(z.describe('Match the max priority fee per gas bounds.'))),
|
|
211
205
|
nonce: z.optional(schema.compare.check(z.describe('Match the nonce bounds.'))),
|
|
@@ -214,9 +208,6 @@ export var schema;
|
|
|
214
208
|
validAfter: z.optional(schema.compare.check(z.describe('Match the `validAfter` bounds (unix s).'))),
|
|
215
209
|
blockNumber: z.optional(schema.compare.check(z.describe('Match the block-number bounds.'))),
|
|
216
210
|
timestamp: z.optional(schema.compare.check(z.describe('Match the block timestamp bounds (unix s).'))),
|
|
217
|
-
status: z.optional(z
|
|
218
|
-
.enum(['success', 'reverted'])
|
|
219
|
-
.check(z.describe('Match the receipt execution status (routes through `receipts`).'), z.meta({ examples: ['success'] }))),
|
|
220
211
|
})
|
|
221
212
|
.check(z.describe('Filters that narrow which included transactions trigger this webhook.'), z.meta({ title: 'Included-transaction filters' }));
|
|
222
213
|
/** A 32-byte log topic (lowercased hex). */
|
|
@@ -402,10 +393,19 @@ export var schema;
|
|
|
402
393
|
})
|
|
403
394
|
.check(z.describe('Payload for a `token:transfer` event, decoded from a TIP-20 `Transfer` log.'));
|
|
404
395
|
/**
|
|
405
|
-
* A `transaction` event payload
|
|
406
|
-
*
|
|
396
|
+
* A `transaction` event payload using the humanized transaction shape without
|
|
397
|
+
* opt-in read resources such as receipts.
|
|
407
398
|
*/
|
|
408
|
-
schema.TransactionEvent =
|
|
399
|
+
schema.TransactionEvent = z
|
|
400
|
+
.object({
|
|
401
|
+
...Transactions.schema.Transaction.shape,
|
|
402
|
+
meta: z
|
|
403
|
+
.object({
|
|
404
|
+
rpc: Transactions.schema.Rpc.Transaction.check(z.describe('The original JSON-RPC transaction payload.')),
|
|
405
|
+
})
|
|
406
|
+
.check(z.describe('Metadata for this transaction webhook event.')),
|
|
407
|
+
})
|
|
408
|
+
.check(z.describe('A transaction included in a block.'));
|
|
409
409
|
/**
|
|
410
410
|
* A `log:emitted` event payload: the raw contract event log. When a
|
|
411
411
|
* `signature` filter was supplied and decoding succeeds, the best-effort
|
|
@@ -461,7 +461,7 @@ export var schema;
|
|
|
461
461
|
.check(z.int(), z.nonnegative(), z.describe('Index of the transaction within its block.'), z.meta({ examples: [0] })),
|
|
462
462
|
})
|
|
463
463
|
.check(z.describe('Payload for a `log:emitted` event (a raw contract event log).'));
|
|
464
|
-
/** Payload for a `block:created` event (
|
|
464
|
+
/** Payload for a `block:created` event (an RPC-derived block header). */
|
|
465
465
|
schema.BlockEvent = z
|
|
466
466
|
.object({
|
|
467
467
|
number: z
|
|
@@ -1317,6 +1317,50 @@ export function webhooks(options = {}) {
|
|
|
1317
1317
|
*/
|
|
1318
1318
|
const endOfBlockIndex = 2_147_483_647;
|
|
1319
1319
|
const scanPageSize = 100;
|
|
1320
|
+
/** Concurrent per-subscription scans and cursor writes per poll round; caps RPC and DB fan-out. */
|
|
1321
|
+
const pollConcurrency = 8;
|
|
1322
|
+
/** Event types scanned together when their combined state fits one existing scan round. */
|
|
1323
|
+
const pollEventTypeConcurrency = 2;
|
|
1324
|
+
/** Maximum subscription states retained by concurrent event-type scan batches. */
|
|
1325
|
+
const pollStateLimit = WebhookTransfer.scanCohortSize * pollConcurrency;
|
|
1326
|
+
/** Schedules asynchronous work through one shared concurrency bound. */
|
|
1327
|
+
function createLimiter(limit) {
|
|
1328
|
+
let active = 0;
|
|
1329
|
+
const waiting = [];
|
|
1330
|
+
return async (fn) => {
|
|
1331
|
+
if (active >= limit)
|
|
1332
|
+
await new Promise((resolve) => waiting.push(resolve));
|
|
1333
|
+
else
|
|
1334
|
+
active++;
|
|
1335
|
+
try {
|
|
1336
|
+
return await fn();
|
|
1337
|
+
}
|
|
1338
|
+
finally {
|
|
1339
|
+
const next = waiting.shift();
|
|
1340
|
+
if (next)
|
|
1341
|
+
next();
|
|
1342
|
+
else
|
|
1343
|
+
active--;
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
/** Runs `fn` over `items` with at most `limit` in flight, preserving order. */
|
|
1348
|
+
async function mapWithLimit(items, limit, fn) {
|
|
1349
|
+
const results = [];
|
|
1350
|
+
let next = 0;
|
|
1351
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
1352
|
+
while (next < items.length) {
|
|
1353
|
+
const index = next++;
|
|
1354
|
+
try {
|
|
1355
|
+
results[index] = { status: 'fulfilled', value: await fn(items[index]) };
|
|
1356
|
+
}
|
|
1357
|
+
catch (reason) {
|
|
1358
|
+
results[index] = { reason, status: 'rejected' };
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}));
|
|
1362
|
+
return results;
|
|
1363
|
+
}
|
|
1320
1364
|
/** Returns `candidate` only when it is lexicographically after `current`. */
|
|
1321
1365
|
function laterCursor(current, candidate) {
|
|
1322
1366
|
const before = Cursor.decode(current, ['int', 'int']);
|
|
@@ -1336,13 +1380,9 @@ function laterCursor(current, candidate) {
|
|
|
1336
1380
|
* matching row, advancing each active subscription's `(block, index)` keyset
|
|
1337
1381
|
* cursor.
|
|
1338
1382
|
*
|
|
1339
|
-
*
|
|
1340
|
-
* `
|
|
1341
|
-
*
|
|
1342
|
-
* `eth_getBlockReceipts`) — the same data sources the indexer itself consumes —
|
|
1343
|
-
* so they need no indexer and stay fresh. The pre-existing `token:transfer`
|
|
1344
|
-
* type continues to read its humanized shape from TIDX, clamped to the indexed
|
|
1345
|
-
* head so it never runs ahead of the indexer.
|
|
1383
|
+
* Every event type scans a **narrow confirmed window** `[cursor, head -
|
|
1384
|
+
* confirmations]` directly over JSON-RPC (`eth_getLogs`,
|
|
1385
|
+
* `eth_getBlockByNumber`) so webhook delivery does not wait for an indexer.
|
|
1346
1386
|
*
|
|
1347
1387
|
* The chain head is injectable (tests pin a fixed historical window); only
|
|
1348
1388
|
* blocks at least `confirmations` behind head are emitted, giving at-least-once,
|
|
@@ -1355,28 +1395,45 @@ function laterCursor(current, candidate) {
|
|
|
1355
1395
|
* after the remaining subscriptions run.
|
|
1356
1396
|
*/
|
|
1357
1397
|
export async function poll(options) {
|
|
1358
|
-
const { batchSize = 200, cache: cache_metadata = Store.memory(), chainId, client, confirmations =
|
|
1398
|
+
const { batchSize = 200, cache: cache_metadata = Store.memory(), chainId, client, confirmations = 1, db, eventTypes: eventTypes_input = [
|
|
1399
|
+
'token:transfer',
|
|
1400
|
+
'transaction:included',
|
|
1401
|
+
'log:emitted',
|
|
1402
|
+
'block:created',
|
|
1403
|
+
], maxBlocks = 1_000, } = options;
|
|
1359
1404
|
const now = options.now ?? (() => new Date());
|
|
1405
|
+
options.signal?.throwIfAborted();
|
|
1360
1406
|
// Default (self-host) transport: deliver inline and record the outcome so a
|
|
1361
1407
|
// sustained-failure subscription auto-disables. On Cloudflare, callers pass a
|
|
1362
1408
|
// Queue producer instead and the consumer runs `deliverAndRecord`.
|
|
1363
1409
|
const dispatch = options.dispatch ??
|
|
1364
1410
|
(({ envelope, subscription }) => Webhooks.deliverAndRecord(db, subscription, envelope));
|
|
1365
|
-
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1411
|
+
const dispatchBatch = options.dispatchBatch;
|
|
1412
|
+
// A pinned `headBlockNumber` lets a head subscription supply the exact live
|
|
1413
|
+
// boundary and keeps historical tests deterministic.
|
|
1368
1414
|
const override = options.headBlockNumber;
|
|
1369
1415
|
let rpcHead = override === undefined ? undefined : Number(override);
|
|
1370
1416
|
const resolveRpcHead = async () => (rpcHead ??= await getRpcHead(client));
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1417
|
+
const headStartedAt = Date.now();
|
|
1418
|
+
const rpcSafeBlock = await (async () => {
|
|
1419
|
+
try {
|
|
1420
|
+
const safeBlock = Math.max(0, (await resolveRpcHead()) - confirmations);
|
|
1421
|
+
options.onStage?.({
|
|
1422
|
+
durationMs: Date.now() - headStartedAt,
|
|
1423
|
+
outcome: 'ok',
|
|
1424
|
+
stage: 'rpc_head',
|
|
1425
|
+
});
|
|
1426
|
+
return safeBlock;
|
|
1427
|
+
}
|
|
1428
|
+
catch (error) {
|
|
1429
|
+
options.onStage?.({
|
|
1430
|
+
durationMs: Date.now() - headStartedAt,
|
|
1431
|
+
outcome: 'error',
|
|
1432
|
+
stage: 'rpc_head',
|
|
1433
|
+
});
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
})();
|
|
1380
1437
|
const result = {
|
|
1381
1438
|
chainId,
|
|
1382
1439
|
dispatched: 0,
|
|
@@ -1385,19 +1442,76 @@ export async function poll(options) {
|
|
|
1385
1442
|
scanned: 0,
|
|
1386
1443
|
subscriptions: 0,
|
|
1387
1444
|
};
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1445
|
+
let cursorLagBlocks = 0;
|
|
1446
|
+
const pollErrors = [];
|
|
1447
|
+
const eventTypes = [...new Set(eventTypes_input)];
|
|
1448
|
+
const subscriptionsStartedAt = Date.now();
|
|
1449
|
+
const pairs = await Webhooks.listActiveForChain(db, { chainId, eventTypes });
|
|
1450
|
+
options.signal?.throwIfAborted();
|
|
1451
|
+
options.onSubscriptions?.(pairs.length);
|
|
1452
|
+
options.onStage?.({
|
|
1453
|
+
durationMs: Date.now() - subscriptionsStartedAt,
|
|
1454
|
+
outcome: 'ok',
|
|
1455
|
+
stage: 'subscription_read',
|
|
1456
|
+
});
|
|
1457
|
+
const pairsByEventType = Map.groupBy(pairs, (pair) => pair.subscription.eventType);
|
|
1458
|
+
const eventTypes_active = eventTypes.filter((eventType) => (pairsByEventType.get(eventType)?.length ?? 0) > 0);
|
|
1459
|
+
const eventTypeConcurrency = pairs.length <= pollStateLimit
|
|
1460
|
+
? Math.min(pollEventTypeConcurrency, eventTypes_active.length)
|
|
1461
|
+
: Math.min(1, eventTypes_active.length);
|
|
1462
|
+
const scanRoundSize = Math.floor(pollStateLimit / Math.max(1, eventTypeConcurrency));
|
|
1463
|
+
const scheduleScan = createLimiter(pollConcurrency);
|
|
1464
|
+
const eventTypeResults = await mapWithLimit(eventTypes_active, eventTypeConcurrency, async (eventType) => {
|
|
1465
|
+
options.signal?.throwIfAborted();
|
|
1466
|
+
const safeBlock = rpcSafeBlock;
|
|
1467
|
+
const pairs = pairsByEventType.get(eventType) ?? [];
|
|
1468
|
+
const durations = {
|
|
1469
|
+
cursor_write: 0,
|
|
1470
|
+
enrich: 0,
|
|
1471
|
+
scan: 0,
|
|
1472
|
+
};
|
|
1473
|
+
const observed = new Set();
|
|
1474
|
+
let cursorWriteOutcome = 'ok';
|
|
1475
|
+
let scanOutcome = 'ok';
|
|
1476
|
+
const writeCursor = async (parameters) => {
|
|
1477
|
+
const startedAt = Date.now();
|
|
1478
|
+
observed.add('cursor_write');
|
|
1479
|
+
try {
|
|
1480
|
+
return await Webhooks.compareAndSetCursor(db, parameters);
|
|
1481
|
+
}
|
|
1482
|
+
catch (error) {
|
|
1483
|
+
cursorWriteOutcome = 'error';
|
|
1484
|
+
pollErrors.push(error);
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
finally {
|
|
1488
|
+
durations.cursor_write += Date.now() - startedAt;
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1393
1491
|
let states = [];
|
|
1394
|
-
for (const
|
|
1492
|
+
for (const pair of pairs) {
|
|
1493
|
+
const { subscription } = pair;
|
|
1395
1494
|
result.subscriptions++;
|
|
1396
|
-
let
|
|
1495
|
+
let filters;
|
|
1496
|
+
try {
|
|
1497
|
+
filters = parseFilters(subscription.eventType, subscription.filters);
|
|
1498
|
+
}
|
|
1499
|
+
catch (error) {
|
|
1500
|
+
if (!(error instanceof Webhooks.InvalidFilterError))
|
|
1501
|
+
throw error;
|
|
1502
|
+
options.signal?.throwIfAborted();
|
|
1503
|
+
// A breaking filter-schema removal must not poison every chain poll.
|
|
1504
|
+
// Disable the legacy subscription without widening its match set.
|
|
1505
|
+
await Webhooks.updateSubscription(db, subscription.owner, subscription.id, {
|
|
1506
|
+
status: 'disabled',
|
|
1507
|
+
});
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
let cursor = pair.cursor;
|
|
1397
1511
|
if (cursor === null) {
|
|
1398
1512
|
// Legacy subscriptions without a creation checkpoint anchor at the
|
|
1399
1513
|
// current confirmed head, avoiding a genesis backfill.
|
|
1400
|
-
await
|
|
1514
|
+
await writeCursor({
|
|
1401
1515
|
cursor: Cursor.encode([safeBlock, endOfBlockIndex]),
|
|
1402
1516
|
expectedCursor: null,
|
|
1403
1517
|
subscriptionId: subscription.id,
|
|
@@ -1412,7 +1526,7 @@ export async function poll(options) {
|
|
|
1412
1526
|
Math.max(0, checkpoint[0] - confirmations),
|
|
1413
1527
|
endOfBlockIndex,
|
|
1414
1528
|
]);
|
|
1415
|
-
const committed = await
|
|
1529
|
+
const committed = await writeCursor({
|
|
1416
1530
|
cursor: confirmedCursor,
|
|
1417
1531
|
expectedCursor: cursor,
|
|
1418
1532
|
subscriptionId: subscription.id,
|
|
@@ -1421,9 +1535,15 @@ export async function poll(options) {
|
|
|
1421
1535
|
continue;
|
|
1422
1536
|
cursor = confirmedCursor;
|
|
1423
1537
|
}
|
|
1424
|
-
const
|
|
1538
|
+
const position = Cursor.decode(cursor, ['int', 'int']);
|
|
1539
|
+
if (position)
|
|
1540
|
+
cursorLagBlocks = Math.max(cursorLagBlocks, safeBlock - position[0]);
|
|
1541
|
+
const window = scanWindow(cursor, safeBlock, maxBlocks);
|
|
1542
|
+
if (window && window.to < safeBlock)
|
|
1543
|
+
result.hasMore = true;
|
|
1425
1544
|
states.push({
|
|
1426
1545
|
cursor,
|
|
1546
|
+
filters,
|
|
1427
1547
|
remaining: batchSize,
|
|
1428
1548
|
safeBlock: window?.to ?? safeBlock,
|
|
1429
1549
|
subscription,
|
|
@@ -1432,87 +1552,219 @@ export async function poll(options) {
|
|
|
1432
1552
|
// Every subscription gets one bounded page before any subscription gets a
|
|
1433
1553
|
// continuation page, preserving fairness while allowing catch-up.
|
|
1434
1554
|
while (states.length > 0) {
|
|
1555
|
+
options.signal?.throwIfAborted();
|
|
1435
1556
|
const continuations = [];
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1557
|
+
const round = states;
|
|
1558
|
+
for (let offset = 0; offset < round.length; offset += scanRoundSize) {
|
|
1559
|
+
const batch = round.slice(offset, offset + scanRoundSize);
|
|
1560
|
+
const windows = batch.flatMap((state) => {
|
|
1561
|
+
const window = scanWindow(state.cursor, state.safeBlock, maxBlocks);
|
|
1562
|
+
return window ? [window] : [];
|
|
1563
|
+
});
|
|
1564
|
+
const maxWindowBlocks = Math.max(0, ...windows.map((window) => window.to - window.from + 1));
|
|
1565
|
+
const totalWindowBlocks = windows.reduce((total, window) => total + window.to - window.from + 1, 0);
|
|
1566
|
+
options.onScanCheckpoint?.({
|
|
1567
|
+
concurrency: pollConcurrency,
|
|
1568
|
+
eventType,
|
|
1569
|
+
maxWindowBlocks,
|
|
1570
|
+
phase: 'start',
|
|
1571
|
+
rpcMethod: scanRpcMethod(eventType),
|
|
1572
|
+
subscriptionLimit: scanRoundSize,
|
|
1573
|
+
subscriptionCount: batch.length,
|
|
1574
|
+
totalWindowBlocks,
|
|
1575
|
+
});
|
|
1576
|
+
const scanStartedAt = Date.now();
|
|
1577
|
+
const pages = await scanSubscriptions({
|
|
1578
|
+
client,
|
|
1579
|
+
eventType,
|
|
1580
|
+
maxBlocks,
|
|
1581
|
+
schedule: scheduleScan,
|
|
1582
|
+
states: batch,
|
|
1583
|
+
});
|
|
1584
|
+
options.onScanCheckpoint?.({
|
|
1585
|
+
concurrency: pollConcurrency,
|
|
1586
|
+
durationMs: Date.now() - scanStartedAt,
|
|
1587
|
+
eventType,
|
|
1588
|
+
failedSubscriptions: pages.filter((page) => page.status === 'rejected').length,
|
|
1589
|
+
matchedItems: pages.reduce((total, page) => total + (page.status === 'fulfilled' ? page.value.items.length : 0), 0),
|
|
1590
|
+
maxWindowBlocks,
|
|
1591
|
+
outcome: scanResultOutcome(pages),
|
|
1592
|
+
phase: 'complete',
|
|
1593
|
+
rpcMethod: scanRpcMethod(eventType),
|
|
1594
|
+
subscriptionLimit: scanRoundSize,
|
|
1595
|
+
subscriptionCount: batch.length,
|
|
1596
|
+
totalWindowBlocks,
|
|
1597
|
+
});
|
|
1598
|
+
options.signal?.throwIfAborted();
|
|
1599
|
+
durations.scan += Date.now() - scanStartedAt;
|
|
1600
|
+
observed.add('scan');
|
|
1601
|
+
if (pages.some((page) => page.status === 'rejected'))
|
|
1602
|
+
scanOutcome = 'error';
|
|
1603
|
+
const busy = [];
|
|
1604
|
+
const idle = [];
|
|
1605
|
+
for (let i = 0; i < batch.length; i++) {
|
|
1606
|
+
const state = batch[i];
|
|
1607
|
+
const settled = pages[i];
|
|
1608
|
+
if (settled.status === 'rejected') {
|
|
1609
|
+
pollErrors.push(settled.reason);
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1612
|
+
result.scanned += settled.value.items.length;
|
|
1613
|
+
state.remaining -= settled.value.items.length;
|
|
1614
|
+
(settled.value.items.length > 0 ? busy : idle).push({ page: settled.value, state });
|
|
1450
1615
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1616
|
+
// Empty pages only advance their cursor: no dispatch order to preserve,
|
|
1617
|
+
// so one statement commits the cohort while retaining per-row CAS semantics.
|
|
1618
|
+
const cursorWritesStartedAt = Date.now();
|
|
1619
|
+
const candidates = idle.flatMap(({ page, state }) => {
|
|
1620
|
+
options.signal?.throwIfAborted();
|
|
1621
|
+
const candidate = page.scannedThrough !== undefined
|
|
1622
|
+
? Cursor.encode([page.scannedThrough, endOfBlockIndex])
|
|
1623
|
+
: page.hasMore
|
|
1624
|
+
? undefined
|
|
1625
|
+
: Cursor.encode([state.safeBlock, endOfBlockIndex]);
|
|
1626
|
+
const cursor = candidate ? laterCursor(state.cursor, candidate) : state.cursor;
|
|
1627
|
+
return cursor === state.cursor ? [] : [{ cursor, state }];
|
|
1628
|
+
});
|
|
1629
|
+
if (candidates.length > 0) {
|
|
1630
|
+
const committed = await (async () => {
|
|
1631
|
+
try {
|
|
1632
|
+
return await Webhooks.compareAndSetCursors(db, {
|
|
1633
|
+
cursors: candidates.map(({ cursor, state }) => ({
|
|
1634
|
+
cursor,
|
|
1635
|
+
expectedCursor: state.cursor,
|
|
1636
|
+
subscriptionId: state.subscription.id,
|
|
1637
|
+
})),
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
catch (error) {
|
|
1641
|
+
cursorWriteOutcome = 'error';
|
|
1642
|
+
pollErrors.push(error);
|
|
1643
|
+
return new Set();
|
|
1644
|
+
}
|
|
1645
|
+
})();
|
|
1646
|
+
for (const { cursor, state } of candidates)
|
|
1647
|
+
if (committed.has(state.subscription.id))
|
|
1648
|
+
state.cursor = cursor;
|
|
1649
|
+
durations.cursor_write += Date.now() - cursorWritesStartedAt;
|
|
1650
|
+
observed.add('cursor_write');
|
|
1454
1651
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1652
|
+
for (const { page, state } of busy) {
|
|
1653
|
+
options.signal?.throwIfAborted();
|
|
1654
|
+
const { subscription } = state;
|
|
1655
|
+
let accepted;
|
|
1656
|
+
let failed = false;
|
|
1657
|
+
const buildDispatchable = async (item) => {
|
|
1658
|
+
options.signal?.throwIfAborted();
|
|
1659
|
+
// Token metadata is best-effort for webhooks: transfers use it for display,
|
|
1660
|
+
// while transactions expose fee-token references without blocking delivery.
|
|
1661
|
+
const enrichStartedAt = Date.now();
|
|
1662
|
+
const data = eventType === 'token:transfer'
|
|
1663
|
+
? await withTokenMetadata(item.data, { cache: cache_metadata, chainId, client })
|
|
1664
|
+
: eventType === 'transaction:included'
|
|
1665
|
+
? await withFeeTokenMetadata(item.data, {
|
|
1666
|
+
cache: cache_metadata,
|
|
1667
|
+
chainId,
|
|
1668
|
+
client,
|
|
1669
|
+
})
|
|
1670
|
+
: item.data;
|
|
1671
|
+
durations.enrich += Date.now() - enrichStartedAt;
|
|
1672
|
+
observed.add('enrich');
|
|
1673
|
+
options.signal?.throwIfAborted();
|
|
1674
|
+
const envelope = Webhooks.buildEnvelope({
|
|
1675
|
+
blockNumber: item.cursor[0],
|
|
1676
|
+
createdAt: now(),
|
|
1677
|
+
data,
|
|
1678
|
+
logIndex: item.cursor[1],
|
|
1679
|
+
subscription,
|
|
1680
|
+
});
|
|
1681
|
+
return { envelope, subscription };
|
|
1682
|
+
};
|
|
1683
|
+
if (dispatchBatch) {
|
|
1684
|
+
const dispatchables = [];
|
|
1685
|
+
for (const item of page.items)
|
|
1686
|
+
dispatchables.push(await buildDispatchable(item));
|
|
1687
|
+
options.signal?.throwIfAborted();
|
|
1688
|
+
const delivered = await dispatchBatch(dispatchables);
|
|
1689
|
+
if (delivered.ok) {
|
|
1690
|
+
accepted = page.items.at(-1)?.cursor;
|
|
1691
|
+
result.dispatched += dispatchables.length;
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
result.failed += dispatchables.length;
|
|
1695
|
+
failed = true;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
else {
|
|
1699
|
+
for (const item of page.items) {
|
|
1700
|
+
const dispatchable = await buildDispatchable(item);
|
|
1701
|
+
const delivered = await dispatch(dispatchable);
|
|
1702
|
+
if (!delivered.ok) {
|
|
1703
|
+
result.failed++;
|
|
1704
|
+
failed = true;
|
|
1705
|
+
break;
|
|
1706
|
+
}
|
|
1707
|
+
accepted = item.cursor;
|
|
1708
|
+
result.dispatched++;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
// Persist every accepted page before another subscription begins its
|
|
1712
|
+
// continuation, so a later scan failure retains completed work.
|
|
1713
|
+
const candidate = (() => {
|
|
1714
|
+
if (failed)
|
|
1715
|
+
return accepted ? Cursor.encode([...accepted]) : undefined;
|
|
1716
|
+
if (page.scannedThrough !== undefined)
|
|
1717
|
+
return Cursor.encode([page.scannedThrough, endOfBlockIndex]);
|
|
1718
|
+
if (accepted)
|
|
1719
|
+
return Cursor.encode([...accepted]);
|
|
1720
|
+
if (!page.hasMore)
|
|
1721
|
+
return Cursor.encode([state.safeBlock, endOfBlockIndex]);
|
|
1722
|
+
return undefined;
|
|
1723
|
+
})();
|
|
1724
|
+
const cursor = candidate ? laterCursor(state.cursor, candidate) : state.cursor;
|
|
1725
|
+
const progressed = cursor !== state.cursor;
|
|
1726
|
+
let committed = false;
|
|
1727
|
+
if (progressed) {
|
|
1728
|
+
committed = await writeCursor({
|
|
1729
|
+
cursor,
|
|
1730
|
+
expectedCursor: state.cursor,
|
|
1731
|
+
subscriptionId: subscription.id,
|
|
1732
|
+
});
|
|
1733
|
+
if (committed)
|
|
1734
|
+
state.cursor = cursor;
|
|
1735
|
+
}
|
|
1736
|
+
if (!failed && page.hasMore === true) {
|
|
1737
|
+
if (committed && state.remaining > 0)
|
|
1738
|
+
continuations.push(state);
|
|
1739
|
+
else
|
|
1740
|
+
result.hasMore = true;
|
|
1479
1741
|
}
|
|
1480
|
-
accepted = item.cursor;
|
|
1481
|
-
result.dispatched++;
|
|
1482
|
-
}
|
|
1483
|
-
// Persist every accepted page before another subscription begins its
|
|
1484
|
-
// continuation, so a later scan failure retains completed work.
|
|
1485
|
-
const candidate = (() => {
|
|
1486
|
-
if (failed)
|
|
1487
|
-
return accepted ? Cursor.encode([...accepted]) : undefined;
|
|
1488
|
-
if (page.scannedThrough !== undefined)
|
|
1489
|
-
return Cursor.encode([page.scannedThrough, endOfBlockIndex]);
|
|
1490
|
-
if (accepted)
|
|
1491
|
-
return Cursor.encode([...accepted]);
|
|
1492
|
-
if (!page.hasMore)
|
|
1493
|
-
return Cursor.encode([state.safeBlock, endOfBlockIndex]);
|
|
1494
|
-
return undefined;
|
|
1495
|
-
})();
|
|
1496
|
-
const cursor = candidate ? laterCursor(state.cursor, candidate) : state.cursor;
|
|
1497
|
-
const progressed = cursor !== state.cursor;
|
|
1498
|
-
let committed = false;
|
|
1499
|
-
if (progressed) {
|
|
1500
|
-
committed = await Webhooks.compareAndSetCursor(db, {
|
|
1501
|
-
cursor,
|
|
1502
|
-
expectedCursor: state.cursor,
|
|
1503
|
-
subscriptionId: subscription.id,
|
|
1504
|
-
});
|
|
1505
|
-
if (committed)
|
|
1506
|
-
state.cursor = cursor;
|
|
1507
1742
|
}
|
|
1508
|
-
if (!failed && page.hasMore === true && committed && state.remaining > 0)
|
|
1509
|
-
continuations.push(state);
|
|
1510
1743
|
}
|
|
1511
1744
|
states = continuations;
|
|
1512
1745
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1746
|
+
for (const stage of observed) {
|
|
1747
|
+
const outcome = (() => {
|
|
1748
|
+
if (stage === 'cursor_write')
|
|
1749
|
+
return cursorWriteOutcome;
|
|
1750
|
+
if (stage === 'scan')
|
|
1751
|
+
return scanOutcome;
|
|
1752
|
+
return 'ok';
|
|
1753
|
+
})();
|
|
1754
|
+
options.onStage?.({
|
|
1755
|
+
durationMs: durations[stage],
|
|
1756
|
+
eventType,
|
|
1757
|
+
outcome,
|
|
1758
|
+
stage,
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1762
|
+
for (const result of eventTypeResults)
|
|
1763
|
+
if (result.status === 'rejected')
|
|
1764
|
+
pollErrors.push(result.reason);
|
|
1765
|
+
options.onCursorLag?.(cursorLagBlocks);
|
|
1766
|
+
if (pollErrors.length > 0)
|
|
1767
|
+
throw pollErrors[0];
|
|
1516
1768
|
return result;
|
|
1517
1769
|
}
|
|
1518
1770
|
/**
|
|
@@ -1583,103 +1835,77 @@ function withResolvedFeeToken(data, metadata) {
|
|
|
1583
1835
|
result['feeToken'] = { address, ...token };
|
|
1584
1836
|
return result;
|
|
1585
1837
|
}
|
|
1586
|
-
/** Adds best-effort fee-token
|
|
1838
|
+
/** Adds a best-effort fee-token reference to a transaction. */
|
|
1587
1839
|
async function withFeeTokenMetadata(data, deps) {
|
|
1588
1840
|
if (typeof data !== 'object' || data === null)
|
|
1589
1841
|
return data;
|
|
1590
1842
|
const transaction = data;
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
? meta['receipt']
|
|
1594
|
-
: undefined;
|
|
1595
|
-
const addresses = new Set();
|
|
1596
|
-
const transactionAddress = feeTokenAddress(transaction['feeToken']);
|
|
1597
|
-
if (transactionAddress)
|
|
1598
|
-
addresses.add(transactionAddress.toLowerCase());
|
|
1599
|
-
if (typeof receipt === 'object' && receipt !== null) {
|
|
1600
|
-
const receiptAddress = feeTokenAddress(receipt['feeToken']);
|
|
1601
|
-
if (receiptAddress)
|
|
1602
|
-
addresses.add(receiptAddress.toLowerCase());
|
|
1603
|
-
}
|
|
1604
|
-
if (addresses.size === 0)
|
|
1843
|
+
const address = feeTokenAddress(transaction['feeToken']);
|
|
1844
|
+
if (!address)
|
|
1605
1845
|
return data;
|
|
1606
|
-
const
|
|
1607
|
-
const metadata = new Map(
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
return {
|
|
1612
|
-
...result,
|
|
1613
|
-
meta: {
|
|
1614
|
-
...meta,
|
|
1615
|
-
receipt: withResolvedFeeToken(receipt, metadata),
|
|
1616
|
-
},
|
|
1617
|
-
};
|
|
1846
|
+
const token = await resolveTokenMetadata(address, deps);
|
|
1847
|
+
const metadata = new Map();
|
|
1848
|
+
if (token)
|
|
1849
|
+
metadata.set(address.toLowerCase(), token);
|
|
1850
|
+
return withResolvedFeeToken(transaction, metadata);
|
|
1618
1851
|
}
|
|
1619
1852
|
/** Returns the live RPC head block number for the chain. */
|
|
1620
1853
|
export async function getRpcHead(client) {
|
|
1621
1854
|
return Number(await client.getBlockNumber());
|
|
1622
1855
|
}
|
|
1623
|
-
/**
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1856
|
+
/** Groups aligned cursors only when their bounded upstream scans are equivalent. */
|
|
1857
|
+
async function scanSubscriptions(options) {
|
|
1858
|
+
const { client, eventType, maxBlocks, schedule, states } = options;
|
|
1859
|
+
if (eventType !== 'token:transfer')
|
|
1860
|
+
return scanIndependent({ client, eventType, maxBlocks, schedule, states });
|
|
1861
|
+
return WebhookTransfer.scan({
|
|
1862
|
+
client,
|
|
1863
|
+
concurrency: pollConcurrency,
|
|
1864
|
+
maxBlocks,
|
|
1865
|
+
schedule,
|
|
1866
|
+
states: states.map((state) => ({
|
|
1867
|
+
filters: state.filters,
|
|
1868
|
+
limit: Math.min(scanPageSize, state.remaining),
|
|
1869
|
+
window: scanWindow(state.cursor, state.safeBlock, maxBlocks),
|
|
1870
|
+
})),
|
|
1632
1871
|
});
|
|
1633
|
-
return Value.toNumber(result.rows[0]?.['num']) ?? 0;
|
|
1634
1872
|
}
|
|
1635
|
-
/**
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
cursor,
|
|
1660
|
-
filters: parseFilters('block:created', subscription.filters),
|
|
1661
|
-
maxBlocks,
|
|
1662
|
-
safeBlock,
|
|
1663
|
-
});
|
|
1664
|
-
if (subscription.eventType === 'transaction:included')
|
|
1665
|
-
return scanTxs({
|
|
1666
|
-
batchSize,
|
|
1873
|
+
/** Returns the bounded JSON-RPC method used by an event scanner. */
|
|
1874
|
+
function scanRpcMethod(eventType) {
|
|
1875
|
+
return eventType === 'block:created' || eventType === 'transaction:included'
|
|
1876
|
+
? 'eth_getBlockByNumber'
|
|
1877
|
+
: 'eth_getLogs';
|
|
1878
|
+
}
|
|
1879
|
+
/** Classifies a completed batch without exposing an unbounded upstream error. */
|
|
1880
|
+
function scanResultOutcome(pages) {
|
|
1881
|
+
const rejected = pages.filter((page) => page.status === 'rejected');
|
|
1882
|
+
if (rejected.some((page) => page.reason instanceof BlockUnavailableError))
|
|
1883
|
+
return 'missing';
|
|
1884
|
+
if (rejected.length > 0)
|
|
1885
|
+
return 'error';
|
|
1886
|
+
return pages.every((page) => page.status === 'fulfilled' && page.value.items.length === 0)
|
|
1887
|
+
? 'empty'
|
|
1888
|
+
: 'ok';
|
|
1889
|
+
}
|
|
1890
|
+
/** Scans CPU-matched streams per subscription while sharing concurrent block reads. */
|
|
1891
|
+
function scanIndependent(options) {
|
|
1892
|
+
const { client, eventType, maxBlocks, schedule, states } = options;
|
|
1893
|
+
const cache = createChainCache(client);
|
|
1894
|
+
return mapWithLimit(states, pollConcurrency, (state) => schedule(() => {
|
|
1895
|
+
const shared = {
|
|
1896
|
+
batchSize: Math.min(scanPageSize, state.remaining),
|
|
1667
1897
|
cache,
|
|
1668
|
-
cursor,
|
|
1669
|
-
filters:
|
|
1898
|
+
cursor: state.cursor,
|
|
1899
|
+
filters: state.filters,
|
|
1670
1900
|
maxBlocks,
|
|
1671
|
-
safeBlock,
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
// would advance the cursor past unseen matches. Hold position and retry.
|
|
1680
|
-
if (page.capped)
|
|
1681
|
-
return { hasMore: true, items: [] };
|
|
1682
|
-
return { hasMore: page.hasMore, items: page.items };
|
|
1901
|
+
safeBlock: state.safeBlock,
|
|
1902
|
+
};
|
|
1903
|
+
if (eventType === 'block:created')
|
|
1904
|
+
return scanBlocks(shared);
|
|
1905
|
+
if (eventType === 'log:emitted')
|
|
1906
|
+
return scanLogs({ ...shared, client });
|
|
1907
|
+
return scanTxs(shared);
|
|
1908
|
+
}));
|
|
1683
1909
|
}
|
|
1684
1910
|
const filterSchemas = {
|
|
1685
1911
|
'block:created': schema.BlockFilters,
|
|
@@ -1699,17 +1925,6 @@ export function parseFilters(eventType, filters) {
|
|
|
1699
1925
|
throw new Webhooks.InvalidFilterError(eventType, Response.validationDetails(result.error.issues));
|
|
1700
1926
|
return result.data;
|
|
1701
1927
|
}
|
|
1702
|
-
/** Narrows stored `transfer` filters into validated `Transfers.scan` predicates. */
|
|
1703
|
-
function transferFilters(filters) {
|
|
1704
|
-
const parsed = schema.TransferFilters.safeParse(filters);
|
|
1705
|
-
const { address, recipient, sender, token } = parsed.success ? parsed.data : {};
|
|
1706
|
-
return {
|
|
1707
|
-
...(address === undefined ? {} : { eitherSide: address }),
|
|
1708
|
-
...(recipient === undefined ? {} : { recipient }),
|
|
1709
|
-
...(sender === undefined ? {} : { sender }),
|
|
1710
|
-
...(token === undefined ? {} : { token }),
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
1928
|
/** Recursively lowercases string leaves of a filter operator (for address args). */
|
|
1714
1929
|
function lowercaseLeaves(op) {
|
|
1715
1930
|
if (typeof op === 'string')
|
|
@@ -1842,52 +2057,34 @@ function scanWindow(cursor, safeBlock, maxBlocks) {
|
|
|
1842
2057
|
function atOrBefore(block, index, skip) {
|
|
1843
2058
|
return block < skip[0] || (block === skip[0] && index <= skip[1]);
|
|
1844
2059
|
}
|
|
1845
|
-
/** Builds a per-
|
|
2060
|
+
/** Builds a per-batch {@link ChainCache} bound to a viem client. */
|
|
1846
2061
|
function createChainCache(client) {
|
|
1847
2062
|
const headers = new Map();
|
|
1848
2063
|
const blocks = new Map();
|
|
1849
|
-
const
|
|
2064
|
+
const singleFlight = (pending, key, load) => {
|
|
2065
|
+
const existing = pending.get(key);
|
|
2066
|
+
if (existing)
|
|
2067
|
+
return existing;
|
|
2068
|
+
const promise = load().finally(() => pending.delete(key));
|
|
2069
|
+
pending.set(key, promise);
|
|
2070
|
+
return promise;
|
|
2071
|
+
};
|
|
1850
2072
|
return {
|
|
1851
2073
|
block(n) {
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
});
|
|
1864
|
-
blocks.set(n, p);
|
|
1865
|
-
}
|
|
1866
|
-
return p;
|
|
2074
|
+
return singleFlight(blocks, n, () =>
|
|
2075
|
+
// The transaction payload reuses `humanizeTransaction`, which consumes
|
|
2076
|
+
// the raw RPC tx shape, so the full block stays on the untyped path.
|
|
2077
|
+
rpc(client, 'eth_getBlockByNumber', [
|
|
2078
|
+
blockTag(n),
|
|
2079
|
+
true,
|
|
2080
|
+
]).then((block) => {
|
|
2081
|
+
if (!block)
|
|
2082
|
+
throw new BlockUnavailableError(n);
|
|
2083
|
+
return block;
|
|
2084
|
+
}));
|
|
1867
2085
|
},
|
|
1868
2086
|
header(n) {
|
|
1869
|
-
|
|
1870
|
-
if (!p) {
|
|
1871
|
-
p = client.getBlock({ blockNumber: BigInt(n), includeTransactions: false });
|
|
1872
|
-
headers.set(n, p);
|
|
1873
|
-
}
|
|
1874
|
-
return p;
|
|
1875
|
-
},
|
|
1876
|
-
receipts(n) {
|
|
1877
|
-
let p = receipts.get(n);
|
|
1878
|
-
if (!p) {
|
|
1879
|
-
p = rpc(client, 'eth_getBlockReceipts', [blockTag(n)]).then((list) => {
|
|
1880
|
-
const map = new Map();
|
|
1881
|
-
for (const receipt of list ?? []) {
|
|
1882
|
-
const hash = receipt['transactionHash'];
|
|
1883
|
-
if (typeof hash === 'string')
|
|
1884
|
-
map.set(hash.toLowerCase(), receipt);
|
|
1885
|
-
}
|
|
1886
|
-
return map;
|
|
1887
|
-
});
|
|
1888
|
-
receipts.set(n, p);
|
|
1889
|
-
}
|
|
1890
|
-
return p;
|
|
2087
|
+
return singleFlight(headers, n, () => client.getBlock({ blockNumber: BigInt(n), includeTransactions: false }));
|
|
1891
2088
|
},
|
|
1892
2089
|
};
|
|
1893
2090
|
}
|
|
@@ -2005,7 +2202,7 @@ function pushTopics(f, event) {
|
|
|
2005
2202
|
pushHex(f.topic2),
|
|
2006
2203
|
pushHex(f.topic3),
|
|
2007
2204
|
];
|
|
2008
|
-
while (topics.at(-1) === undefined)
|
|
2205
|
+
while (topics.length > 0 && topics.at(-1) === undefined)
|
|
2009
2206
|
topics.pop();
|
|
2010
2207
|
return topics.map((topic) => topic ?? null);
|
|
2011
2208
|
}
|
|
@@ -2040,17 +2237,7 @@ function decodeScanLog(log, event) {
|
|
|
2040
2237
|
return log;
|
|
2041
2238
|
}
|
|
2042
2239
|
}
|
|
2043
|
-
/**
|
|
2044
|
-
* Scans contract event logs over the confirmed window via `eth_getLogs`,
|
|
2045
|
-
* then re-checks **every** filter in JS. Operators the RPC cannot express
|
|
2046
|
-
* (`not`, ranges, non-indexed `args`) still apply. Two modes:
|
|
2047
|
-
* - **raw** (no `signature`): `topic0..3` are matched directly.
|
|
2048
|
-
* - **decoded** (`signature` present): the selector, raw topics, and supported
|
|
2049
|
-
* indexed `args` are pushed down before the JS pass matches decoded values.
|
|
2050
|
-
*
|
|
2051
|
-
* Exact and membership address/topic operators are pushed down to the node;
|
|
2052
|
-
* all operators are re-checked in JS over the narrow window.
|
|
2053
|
-
*/
|
|
2240
|
+
/** Scans contract logs for one subscription, then re-checks every filter in memory. */
|
|
2054
2241
|
async function scanLogs(options) {
|
|
2055
2242
|
const { batchSize, cache, client, cursor, filters, maxBlocks, safeBlock } = options;
|
|
2056
2243
|
const f = filters;
|
|
@@ -2063,8 +2250,6 @@ async function scanLogs(options) {
|
|
|
2063
2250
|
{ message: 'Unsupported event signature.', path: ['signature'] },
|
|
2064
2251
|
]);
|
|
2065
2252
|
const address = pushAddress(f.address);
|
|
2066
|
-
const fromBlock = BigInt(window.from);
|
|
2067
|
-
const toBlock = BigInt(window.to);
|
|
2068
2253
|
const topics = pushTopics(f, event);
|
|
2069
2254
|
const hasRawTopics = topics.slice(1).some((topic) => topic !== null);
|
|
2070
2255
|
const args = event ? pushEventArgs(f.args, event) : {};
|
|
@@ -2083,16 +2268,13 @@ async function scanLogs(options) {
|
|
|
2083
2268
|
...(address ? { address } : {}),
|
|
2084
2269
|
...(Object.keys(args).length > 0 ? { args: args } : {}),
|
|
2085
2270
|
event,
|
|
2086
|
-
fromBlock,
|
|
2271
|
+
fromBlock: BigInt(window.from),
|
|
2087
2272
|
strict: false,
|
|
2088
|
-
toBlock,
|
|
2273
|
+
toBlock: BigInt(window.to),
|
|
2089
2274
|
}));
|
|
2090
2275
|
logs.sort((a, b) => {
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2093
|
-
if (ab !== bb)
|
|
2094
|
-
return ab - bb;
|
|
2095
|
-
return (a.logIndex ?? 0) - (b.logIndex ?? 0);
|
|
2276
|
+
const block = Number(a.blockNumber ?? 0n) - Number(b.blockNumber ?? 0n);
|
|
2277
|
+
return block || (a.logIndex ?? 0) - (b.logIndex ?? 0);
|
|
2096
2278
|
});
|
|
2097
2279
|
const items = [];
|
|
2098
2280
|
for (const log of logs) {
|
|
@@ -2104,9 +2286,7 @@ async function scanLogs(options) {
|
|
|
2104
2286
|
continue;
|
|
2105
2287
|
if (!matchesLog(log, f, event))
|
|
2106
2288
|
continue;
|
|
2107
|
-
const
|
|
2108
|
-
const timestamp = blockIso(header);
|
|
2109
|
-
const data = toLogEvent(log, timestamp, f.signature, event);
|
|
2289
|
+
const data = toLogEvent(log, blockIso(await cache.header(blockNumber)), f.signature, event);
|
|
2110
2290
|
if (data) {
|
|
2111
2291
|
items.push({ cursor: [data.blockNumber, data.logIndex], data });
|
|
2112
2292
|
if (items.length >= batchSize)
|
|
@@ -2118,6 +2298,11 @@ async function scanLogs(options) {
|
|
|
2118
2298
|
/** Re-checks all log filters (including non-pushable operators) in JS. */
|
|
2119
2299
|
function matchesLog(log, f, event) {
|
|
2120
2300
|
const topics = log.topics;
|
|
2301
|
+
if (event) {
|
|
2302
|
+
const selector = encodeEventTopics({ abi: [event] })[0];
|
|
2303
|
+
if (topics[0]?.toLowerCase() !== selector.toLowerCase())
|
|
2304
|
+
return false;
|
|
2305
|
+
}
|
|
2121
2306
|
if (f.address !== undefined && !matchesOperator(log.address, f.address))
|
|
2122
2307
|
return false;
|
|
2123
2308
|
if (f.blockNumber !== undefined &&
|
|
@@ -2189,11 +2374,7 @@ function toBlockEvent(block) {
|
|
|
2189
2374
|
transactionCount,
|
|
2190
2375
|
};
|
|
2191
2376
|
}
|
|
2192
|
-
/**
|
|
2193
|
-
* Scans new blocks over the confirmed window via viem's `getBlock`. One row per
|
|
2194
|
-
* block (the keyset index is always 0); filters match block-level metadata. An
|
|
2195
|
-
* empty filter is a per-block heartbeat.
|
|
2196
|
-
*/
|
|
2377
|
+
/** Scans new blocks over one subscription's confirmed window. */
|
|
2197
2378
|
async function scanBlocks(options) {
|
|
2198
2379
|
const { batchSize, cache, cursor, filters, maxBlocks, safeBlock } = options;
|
|
2199
2380
|
const f = filters;
|
|
@@ -2201,10 +2382,10 @@ async function scanBlocks(options) {
|
|
|
2201
2382
|
if (!window)
|
|
2202
2383
|
return { items: [], scannedThrough: undefined };
|
|
2203
2384
|
const items = [];
|
|
2204
|
-
for (let
|
|
2205
|
-
if (atOrBefore(
|
|
2385
|
+
for (let blockNumber = window.from; blockNumber <= window.to; blockNumber++) {
|
|
2386
|
+
if (atOrBefore(blockNumber, 0, window.skip))
|
|
2206
2387
|
continue;
|
|
2207
|
-
const block = await cache.header(
|
|
2388
|
+
const block = await cache.header(blockNumber);
|
|
2208
2389
|
if (!block)
|
|
2209
2390
|
continue;
|
|
2210
2391
|
if (!matchesBlock(block, f))
|
|
@@ -2237,7 +2418,7 @@ function matchesBlock(block, f) {
|
|
|
2237
2418
|
return false;
|
|
2238
2419
|
return true;
|
|
2239
2420
|
}
|
|
2240
|
-
// transaction:included (eth_getBlockByNumber
|
|
2421
|
+
// transaction:included (eth_getBlockByNumber)
|
|
2241
2422
|
/** Maps a `transaction:included` scalar filter field to its raw-tx field + kind. */
|
|
2242
2423
|
const txFields = {
|
|
2243
2424
|
blockNumber: { field: 'blockNumber', kind: 'number' },
|
|
@@ -2253,13 +2434,7 @@ const txFields = {
|
|
|
2253
2434
|
validBefore: { field: 'validBefore', kind: 'number' },
|
|
2254
2435
|
value: { field: 'value', kind: 'number' },
|
|
2255
2436
|
};
|
|
2256
|
-
/**
|
|
2257
|
-
* Scans included transactions over the confirmed window. Each block is fetched
|
|
2258
|
-
* with full transactions via `eth_getBlockByNumber(n, true)`; receipts are
|
|
2259
|
-
* fetched lazily (only when a `status`/`gasUsed`/`feePayer` filter needs them).
|
|
2260
|
-
* The delivered payload starts from `Transactions.humanizeTransaction`; the
|
|
2261
|
-
* poller then resolves its fee-token references.
|
|
2262
|
-
*/
|
|
2437
|
+
/** Scans included transactions over one subscription's confirmed window. */
|
|
2263
2438
|
async function scanTxs(options) {
|
|
2264
2439
|
const { batchSize, cache, cursor, filters, maxBlocks, safeBlock } = options;
|
|
2265
2440
|
const f = filters;
|
|
@@ -2267,54 +2442,33 @@ async function scanTxs(options) {
|
|
|
2267
2442
|
if (!window)
|
|
2268
2443
|
return { items: [], scannedThrough: undefined };
|
|
2269
2444
|
const includeCalls = f.includeCalls ?? true;
|
|
2270
|
-
const includeReceipt = f.includeReceipt ?? false;
|
|
2271
|
-
// Filter-driven receipt need gates matching; `includeReceipt` only enriches the
|
|
2272
|
-
// payload, so it never drops a row when the receipt is missing.
|
|
2273
|
-
const needsReceipt = f.status !== undefined || f.gasUsed !== undefined || f.feePayer !== undefined;
|
|
2274
2445
|
const items = [];
|
|
2275
|
-
for (let
|
|
2276
|
-
const block = await cache.block(
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
const receipts = needsReceipt || includeReceipt ? await cache.receipts(n) : undefined;
|
|
2446
|
+
for (let blockNumber = window.from; blockNumber <= window.to; blockNumber++) {
|
|
2447
|
+
const block = await cache.block(blockNumber);
|
|
2448
|
+
const transactions = Array.isArray(block['transactions'])
|
|
2449
|
+
? block['transactions']
|
|
2450
|
+
: [];
|
|
2281
2451
|
const timestamp = asString(block['timestamp']);
|
|
2282
|
-
for (let
|
|
2283
|
-
const
|
|
2284
|
-
const
|
|
2285
|
-
if (atOrBefore(
|
|
2286
|
-
continue;
|
|
2287
|
-
const receipt = receipts?.get(String(tx['hash']).toLowerCase());
|
|
2288
|
-
if (needsReceipt && !receipt)
|
|
2452
|
+
for (let offset = 0; offset < transactions.length; offset++) {
|
|
2453
|
+
const transaction = transactions[offset];
|
|
2454
|
+
const transactionIndex = Value.hexToNumber(transaction['transactionIndex']) ?? offset;
|
|
2455
|
+
if (atOrBefore(blockNumber, transactionIndex, window.skip))
|
|
2289
2456
|
continue;
|
|
2290
|
-
if (!matchesTx(
|
|
2457
|
+
if (!matchesTx(transaction, f, includeCalls, timestamp, blockNumber))
|
|
2291
2458
|
continue;
|
|
2292
|
-
const parsed = Transactions.schema.Rpc.Transaction.safeParse(
|
|
2459
|
+
const parsed = Transactions.schema.Rpc.Transaction.safeParse(transaction);
|
|
2293
2460
|
if (!parsed.success)
|
|
2294
2461
|
continue;
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
// endpoint's `include=receipt` shape so `data` stays a valid envelope.
|
|
2298
|
-
if (includeReceipt && receipt) {
|
|
2299
|
-
const r = Receipts.schema.Rpc.Receipt.safeParse(receipt);
|
|
2300
|
-
if (r.success)
|
|
2301
|
-
data = {
|
|
2302
|
-
...data,
|
|
2303
|
-
meta: {
|
|
2304
|
-
...data['meta'],
|
|
2305
|
-
receipt: Receipts.humanizeReceipt(r.data),
|
|
2306
|
-
},
|
|
2307
|
-
};
|
|
2308
|
-
}
|
|
2309
|
-
items.push({ cursor: [n, index], data });
|
|
2462
|
+
const data = Transactions.humanizeTransaction(parsed.data);
|
|
2463
|
+
items.push({ cursor: [blockNumber, transactionIndex], data });
|
|
2310
2464
|
if (items.length >= batchSize)
|
|
2311
2465
|
return { hasMore: true, items };
|
|
2312
2466
|
}
|
|
2313
2467
|
}
|
|
2314
2468
|
return { items, scannedThrough: window.to };
|
|
2315
2469
|
}
|
|
2316
|
-
/** Re-checks
|
|
2317
|
-
function matchesTx(tx, f, includeCalls,
|
|
2470
|
+
/** Re-checks transaction filters against the raw RPC transaction. */
|
|
2471
|
+
function matchesTx(tx, f, includeCalls, timestamp, blockNumber) {
|
|
2318
2472
|
const from = tx['from'];
|
|
2319
2473
|
const to = tx['to'];
|
|
2320
2474
|
const calls = tx['calls'];
|
|
@@ -2351,18 +2505,6 @@ function matchesTx(tx, f, includeCalls, receipt, timestamp, blockNumber) {
|
|
|
2351
2505
|
}
|
|
2352
2506
|
if (f.timestamp !== undefined && !matchesOperator(timestamp, f.timestamp, 'number'))
|
|
2353
2507
|
return false;
|
|
2354
|
-
if (receipt) {
|
|
2355
|
-
if (f.feePayer !== undefined && !matchesOperator(asString(receipt['feePayer']), f.feePayer))
|
|
2356
|
-
return false;
|
|
2357
|
-
if (f.gasUsed !== undefined &&
|
|
2358
|
-
!matchesOperator(asString(receipt['gasUsed']), f.gasUsed, 'number'))
|
|
2359
|
-
return false;
|
|
2360
|
-
if (f.status !== undefined) {
|
|
2361
|
-
const ok = Value.hexToNumber(receipt['status']) === 1;
|
|
2362
|
-
if ((f.status === 'success') !== ok)
|
|
2363
|
-
return false;
|
|
2364
|
-
}
|
|
2365
|
-
}
|
|
2366
2508
|
return true;
|
|
2367
2509
|
}
|
|
2368
2510
|
/** Coerces an RPC field to a string for matching, or `undefined` when absent. */
|