tapimo 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ApiKey.d.ts +2 -1
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKeys.d.ts +4 -2
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +26 -1
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +4292 -3708
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +1 -0
- 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/App.d.ts +91 -84
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +26 -17
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +6 -0
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +4 -2
- package/dist/admin/apps/organizations.d.ts.map +1 -1
- package/dist/admin/apps/verified-tokens.d.ts +168 -168
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.js +69 -69
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +26 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/apps/Zones.d.ts +4280 -84
- package/dist/apps/Zones.d.ts.map +1 -1
- package/dist/apps/Zones.js +30 -10
- package/dist/apps/Zones.js.map +1 -1
- package/dist/apps/data/App.d.ts +5799 -4929
- 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 +3391 -492
- package/dist/apps/data/routes/activities.d.ts.map +1 -1
- package/dist/apps/data/routes/activities.js +372 -29
- package/dist/apps/data/routes/activities.js.map +1 -1
- package/dist/apps/data/routes/addresses.d.ts +853 -339
- package/dist/apps/data/routes/addresses.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.d.ts +10 -10
- package/dist/apps/data/routes/balances.d.ts.map +1 -1
- package/dist/apps/data/routes/balances.js +145 -83
- package/dist/apps/data/routes/balances.js.map +1 -1
- package/dist/apps/data/routes/blocks.d.ts +123 -123
- package/dist/apps/data/routes/coingecko.d.ts +169 -169
- package/dist/apps/data/routes/earn.d.ts +455 -1
- package/dist/apps/data/routes/earn.d.ts.map +1 -1
- package/dist/apps/data/routes/earn.js +369 -45
- package/dist/apps/data/routes/earn.js.map +1 -1
- 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/fee-amm.d.ts +24 -24
- package/dist/apps/data/routes/indexer.d.ts +35 -34
- package/dist/apps/data/routes/indexer.d.ts.map +1 -1
- package/dist/apps/data/routes/indexer.js +3 -2
- package/dist/apps/data/routes/indexer.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/rpc.d.ts.map +1 -1
- package/dist/apps/data/routes/rpc.js +5 -1
- package/dist/apps/data/routes/rpc.js.map +1 -1
- package/dist/apps/data/routes/tokenlist.d.ts +12 -12
- 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 +313 -305
- 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/webhooks.d.ts +519 -868
- package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/data/routes/webhooks.js +642 -353
- package/dist/apps/data/routes/webhooks.js.map +1 -1
- package/dist/apps/data/routes/zones.d.ts +31 -31
- package/dist/apps/data/routes/zones.d.ts.map +1 -1
- package/dist/apps/data/routes/zones.js +4 -2
- package/dist/apps/data/routes/zones.js.map +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js +1 -1
- package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
- package/dist/apps/funding/routes/providers.d.ts +13 -13
- package/dist/apps/funding/routes/quotes.d.ts +89 -89
- package/dist/apps/funding/routes/transfers.d.ts +36 -36
- package/dist/apps/funding/routes/transfers.js +1 -1
- package/dist/apps/funding/routes/transfers.js.map +1 -1
- package/dist/apps/management/App.d.ts +11 -309
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +74 -74
- package/dist/apps/management/routes/billing.js +1 -1
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/faucet.d.ts +35 -35
- package/dist/apps/management/routes/faucet.js +2 -2
- package/dist/apps/management/routes/faucet.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +134 -134
- package/dist/apps/management/routes/invite-links.d.ts +80 -80
- package/dist/apps/management/routes/members.d.ts +69 -69
- package/dist/apps/management/routes/orgs.d.ts +98 -98
- package/dist/apps/management/routes/projects.d.ts +60 -60
- package/dist/apps/management/routes/usage.d.ts +72 -72
- package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
- package/dist/apps/management/routes/webhooks.d.ts +203 -618
- package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +4 -1
- package/dist/apps/mcp.js.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 +4 -2
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +75 -15
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +19 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +24 -0
- package/dist/db/Schema.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/webhookQueueEvents.d.ts +122 -0
- package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
- package/dist/db/tables/webhookQueueEvents.js +160 -0
- package/dist/db/tables/webhookQueueEvents.js.map +1 -0
- package/dist/db/tables/webhookSubscriptions.d.ts +58 -3
- package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
- package/dist/db/tables/webhookSubscriptions.js +86 -3
- package/dist/db/tables/webhookSubscriptions.js.map +1 -1
- package/dist/internal/Auth.d.ts +3 -0
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +6 -4
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/MetricSink.d.ts +139 -3
- package/dist/internal/MetricSink.d.ts.map +1 -1
- package/dist/internal/MetricSink.js +144 -4
- package/dist/internal/MetricSink.js.map +1 -1
- package/dist/internal/RateLimit.d.ts +4 -6
- package/dist/internal/RateLimit.d.ts.map +1 -1
- package/dist/internal/RateLimit.js +15 -14
- package/dist/internal/RateLimit.js.map +1 -1
- package/dist/internal/Schema.js +1 -1
- package/dist/internal/Schema.js.map +1 -1
- package/dist/internal/Store.d.ts +2 -1
- package/dist/internal/Store.d.ts.map +1 -1
- package/dist/internal/Store.js +57 -18
- package/dist/internal/Store.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/Value.d.ts +2 -0
- package/dist/internal/Value.d.ts.map +1 -1
- package/dist/internal/Value.js +25 -0
- package/dist/internal/Value.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 +75 -0
- package/dist/internal/WebhookTransfer.d.ts.map +1 -0
- package/dist/internal/WebhookTransfer.js +299 -0
- package/dist/internal/WebhookTransfer.js.map +1 -0
- package/dist/internal/Webhooks.d.ts +176 -0
- package/dist/internal/Webhooks.d.ts.map +1 -1
- package/dist/internal/Webhooks.js +216 -20
- 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 +6 -4
- package/dist/internal/funding/providers/relay.d.ts.map +1 -1
- package/dist/internal/funding/providers/relay.js +71 -9
- package/dist/internal/funding/providers/relay.js.map +1 -1
- package/package.json +4 -4
- package/src/ApiKeys.test.ts +20 -0
- package/src/ApiKeys.ts +28 -1
- package/src/App.test.ts +10 -8
- package/src/App.ts +1 -0
- package/src/Client.test-d.ts +61 -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 +85 -29
- package/src/admin/apps/api-keys.test.ts +9 -0
- package/src/admin/apps/api-keys.ts +15 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -2
- package/src/admin/ui/src/routes/api-keys.tsx +31 -9
- package/src/admin/ui.generated.ts +69 -69
- package/src/analytics/Analytics.test.ts +8 -0
- package/src/analytics/Analytics.ts +26 -0
- package/src/apps/Zones.test.ts +23 -5
- package/src/apps/Zones.ts +31 -10
- package/src/apps/composition.test.ts +1 -0
- package/src/apps/data/App.test.ts +2 -2
- package/src/apps/data/FxOracle.test.ts +18 -0
- package/src/apps/data/FxOracle.ts +28 -5
- package/src/apps/data/routes/activities.bench.ts +41 -0
- package/src/apps/data/routes/activities.test.ts +122 -2
- package/src/apps/data/routes/activities.ts +435 -32
- package/src/apps/data/routes/balances.bench.ts +21 -0
- package/src/apps/data/routes/balances.test.ts +119 -10
- package/src/apps/data/routes/balances.ts +164 -97
- package/src/apps/data/routes/blocks.bench.ts +34 -0
- package/src/apps/data/routes/blocks.test.ts +12 -6
- package/src/apps/data/routes/coingecko.bench.ts +53 -0
- package/src/apps/data/routes/earn.bench.ts +71 -0
- package/src/apps/data/routes/earn.test.ts +197 -0
- package/src/apps/data/routes/earn.ts +487 -45
- package/src/apps/data/routes/exchanges.bench.ts +85 -0
- package/src/apps/data/routes/exchanges.test.ts +7 -6
- package/src/apps/data/routes/exchanges.ts +124 -117
- package/src/apps/data/routes/fee-amm.bench.ts +13 -0
- package/src/apps/data/routes/indexer.bench.ts +8 -0
- package/src/apps/data/routes/indexer.test.ts +2 -1
- package/src/apps/data/routes/indexer.ts +3 -2
- package/src/apps/data/routes/receipts.bench.ts +39 -0
- package/src/apps/data/routes/receipts.test.ts +24 -7
- package/src/apps/data/routes/receipts.ts +67 -50
- package/src/apps/data/routes/rpc.bench.ts +14 -0
- package/src/apps/data/routes/rpc.test.ts +27 -0
- package/src/apps/data/routes/rpc.ts +5 -1
- package/src/apps/data/routes/tokenlist.bench.ts +3 -0
- package/src/apps/data/routes/tokens.bench.ts +76 -0
- package/src/apps/data/routes/tokens.test.ts +51 -0
- package/src/apps/data/routes/transactions.bench.ts +40 -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.bench.ts +28 -0
- 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.bench.ts +14 -0
- package/src/apps/data/routes/valuation.test.ts +26 -0
- package/src/apps/data/routes/valuation.ts +6 -2
- package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
- package/src/apps/data/routes/webhooks.bench.ts +26 -0
- package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
- package/src/apps/data/routes/webhooks.test.ts +466 -135
- package/src/apps/data/routes/webhooks.ts +820 -429
- package/src/apps/data/routes/zones.bench.ts +32 -0
- package/src/apps/data/routes/zones.test.ts +1 -1
- package/src/apps/data/routes/zones.ts +7 -2
- package/src/apps/funding/routes/chains.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
- package/src/apps/funding/routes/deposit-addresses.ts +1 -1
- package/src/apps/funding/routes/providers.test.ts +1 -1
- package/src/apps/funding/routes/quotes.test.ts +2 -2
- package/src/apps/funding/routes/transfers.test.ts +3 -6
- package/src/apps/funding/routes/transfers.ts +1 -1
- package/src/apps/management/routes/billing.ts +1 -1
- package/src/apps/management/routes/faucet.ts +2 -2
- package/src/apps/management/routes/me.test.ts +31 -0
- package/src/apps/mcp.ts +4 -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 +114 -19
- package/src/db/Schema.ts +32 -0
- package/src/db/tables/sponsoredTransactions.ts +9 -2
- package/src/db/tables/webhookQueueEvents.ts +285 -0
- package/src/db/tables/webhookSubscriptions.ts +131 -4
- package/src/internal/Auth.test.ts +61 -50
- package/src/internal/Auth.ts +9 -4
- package/src/internal/EarnVaults.test.ts +2 -2
- package/src/internal/MetricSink.test.ts +575 -0
- package/src/internal/MetricSink.ts +325 -10
- package/src/internal/RateLimit.test.ts +23 -18
- package/src/internal/RateLimit.ts +23 -15
- package/src/internal/Schema.ts +1 -1
- package/src/internal/Store.test.ts +99 -13
- package/src/internal/Store.ts +52 -17
- package/src/internal/Tidx.test.ts +4 -4
- package/src/internal/Tidx.ts +1 -3
- package/src/internal/Value.test.ts +25 -0
- package/src/internal/Value.ts +29 -0
- 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.test.ts +89 -0
- package/src/internal/WebhookTransfer.ts +430 -0
- package/src/internal/Webhooks.test.ts +616 -0
- package/src/internal/Webhooks.ts +412 -24
- package/src/internal/funding/Provider.test.ts +48 -8
- 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/index.test.ts +73 -3
- package/src/internal/funding/providers/relay.test.ts +2 -9
- package/src/internal/funding/providers/relay.ts +72 -13
package/src/internal/Webhooks.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Hash, Hex } from 'ox'
|
|
|
3
3
|
import type * as app_Webhooks from '../apps/data/routes/webhooks.js'
|
|
4
4
|
import type * as Db from '../db/Db.js'
|
|
5
5
|
import * as WebhookDeliveries from '../db/tables/webhookDeliveries.js'
|
|
6
|
+
import * as WebhookQueueEvents from '../db/tables/webhookQueueEvents.js'
|
|
6
7
|
import * as WebhookSubscriptions from '../db/tables/webhookSubscriptions.js'
|
|
7
8
|
import type * as Metrics from '../Metrics.js'
|
|
8
9
|
import * as Cursor from './Cursor.js'
|
|
@@ -326,9 +327,59 @@ export async function listActive(
|
|
|
326
327
|
chainId: number,
|
|
327
328
|
eventType: EventType,
|
|
328
329
|
): Promise<Subscription[]> {
|
|
330
|
+
const pairs = await listActiveWithCursors(db, chainId, eventType)
|
|
331
|
+
return pairs.map((pair) => pair.subscription)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Lists a stream's live active subscriptions paired with their poller cursors,
|
|
336
|
+
* from one query. The poller's per-tick lookup.
|
|
337
|
+
*/
|
|
338
|
+
export async function listActiveWithCursors(
|
|
339
|
+
db: Db.Db,
|
|
340
|
+
chainId: number,
|
|
341
|
+
eventType: EventType,
|
|
342
|
+
): Promise<{ cursor: string | null; subscription: Subscription }[]> {
|
|
329
343
|
return WebhookSubscriptions.listActive(db, chainId, eventType, new Date().toISOString())
|
|
330
344
|
}
|
|
331
345
|
|
|
346
|
+
/** Reads every live active subscription on a chain in one poller query. */
|
|
347
|
+
export async function listActiveForChain(
|
|
348
|
+
db: Db.Db,
|
|
349
|
+
options: listActiveForChain.Options,
|
|
350
|
+
): Promise<{ cursor: string | null; subscription: Subscription }[]> {
|
|
351
|
+
return WebhookSubscriptions.listActiveForChain(db, {
|
|
352
|
+
chainId: options.chainId,
|
|
353
|
+
eventTypes: options.eventTypes,
|
|
354
|
+
now: new Date().toISOString(),
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export declare namespace listActiveForChain {
|
|
359
|
+
/** Filters for the poller's chain-level subscription read. */
|
|
360
|
+
type Options = Pick<WebhookSubscriptions.listActiveForChain.Options, 'chainId' | 'eventTypes'>
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Reads the freshest cursor block across a chain's active subscriptions, or
|
|
365
|
+
* null when none carry a cursor. Creation checkpoints count at their recorded
|
|
366
|
+
* head, so a brand-new subscription reads as fresh.
|
|
367
|
+
*/
|
|
368
|
+
export async function maxCursorBlock(db: Db.Db, chainId: number): Promise<number | null> {
|
|
369
|
+
const cursors = await WebhookSubscriptions.listActiveCursors(
|
|
370
|
+
db,
|
|
371
|
+
chainId,
|
|
372
|
+
new Date().toISOString(),
|
|
373
|
+
)
|
|
374
|
+
let max: number | null = null
|
|
375
|
+
for (const cursor of cursors) {
|
|
376
|
+
const decoded = Cursor.decode(cursor, ['int', 'int']) ?? Cursor.decode(cursor, ['int'])
|
|
377
|
+
const block = decoded?.[0]
|
|
378
|
+
if (typeof block === 'number' && (max === null || block > max)) max = block
|
|
379
|
+
}
|
|
380
|
+
return max
|
|
381
|
+
}
|
|
382
|
+
|
|
332
383
|
/** Reads a subscription's keyset cursor. Returns null when unset. */
|
|
333
384
|
export async function getCursor(db: Db.Db, subscriptionId: string): Promise<string | null> {
|
|
334
385
|
return WebhookSubscriptions.getCursor(db, subscriptionId)
|
|
@@ -347,6 +398,19 @@ export declare namespace compareAndSetCursor {
|
|
|
347
398
|
type Options = WebhookSubscriptions.compareAndSetCursor.Options
|
|
348
399
|
}
|
|
349
400
|
|
|
401
|
+
/** Writes cursors whose stored values match their expected cursors. */
|
|
402
|
+
export async function compareAndSetCursors(
|
|
403
|
+
db: Db.Db,
|
|
404
|
+
options: compareAndSetCursors.Options,
|
|
405
|
+
): Promise<Set<string>> {
|
|
406
|
+
return WebhookSubscriptions.compareAndSetCursors(db, options)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export declare namespace compareAndSetCursors {
|
|
410
|
+
/** Options for {@link compareAndSetCursors}. */
|
|
411
|
+
type Options = WebhookSubscriptions.compareAndSetCursors.Options
|
|
412
|
+
}
|
|
413
|
+
|
|
350
414
|
/** Writes a subscription's keyset cursor. */
|
|
351
415
|
export async function setCursor(db: Db.Db, subscriptionId: string, cursor: string): Promise<void> {
|
|
352
416
|
await WebhookSubscriptions.setCursor(db, subscriptionId, cursor)
|
|
@@ -360,6 +424,7 @@ export async function setCursor(db: Db.Db, subscriptionId: string, cursor: strin
|
|
|
360
424
|
export async function pruneExpired(db: Db.Db, options: pruneExpired.Options = {}): Promise<void> {
|
|
361
425
|
const iso = (options.now ?? (() => new Date()))().toISOString()
|
|
362
426
|
await WebhookDeliveries.pruneExpired(db, iso)
|
|
427
|
+
await WebhookQueueEvents.pruneExpired(db, iso)
|
|
363
428
|
await WebhookSubscriptions.pruneExpired(db, iso)
|
|
364
429
|
}
|
|
365
430
|
|
|
@@ -377,6 +442,263 @@ export declare namespace pruneExpired {
|
|
|
377
442
|
*/
|
|
378
443
|
export const deliveryRetentionMs = 7 * 24 * 60 * 60 * 1_000
|
|
379
444
|
|
|
445
|
+
/** A compact Cloudflare Queue message referencing a durable staged envelope. */
|
|
446
|
+
export type QueueReference = {
|
|
447
|
+
/** Idempotent event id of the staged envelope. */
|
|
448
|
+
eventId: string
|
|
449
|
+
/** Subscription that owns the staged envelope. */
|
|
450
|
+
subscriptionId: string
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** A full envelope staged outside Cloudflare Queues. */
|
|
454
|
+
export type QueueEvent = QueueReference & {
|
|
455
|
+
/** ISO timestamp when the envelope was staged. */
|
|
456
|
+
createdAt: string
|
|
457
|
+
/** Full envelope loaded by the Queue consumer. */
|
|
458
|
+
envelope: Envelope
|
|
459
|
+
/** ISO timestamp after which an unconsumed envelope may be pruned. */
|
|
460
|
+
expiresAt: string
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** A claim newer than this counts as an in-flight attempt; above the 10s destination timeout default. */
|
|
464
|
+
export const deliveryAttemptStaleMs = 60_000
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Retry backoff by attempt count: 5s, 25s, ~2m, ~10m, ~52m, then hourly.
|
|
468
|
+
* Endpoint outages retry on this schedule until delivery or auto-disable.
|
|
469
|
+
*/
|
|
470
|
+
export function retryDelayMs(attemptCount: number): number {
|
|
471
|
+
return Math.min(5_000 * 5 ** Math.max(0, attemptCount - 1), 3_600_000)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Stages full envelopes as delivery obligations and returns compact
|
|
476
|
+
* references safe for Queue admission. Existing rows are left untouched, so
|
|
477
|
+
* the first staged envelope wins and terminal outcomes never resurrect.
|
|
478
|
+
*/
|
|
479
|
+
export async function ensureQueueEvents(
|
|
480
|
+
db: Db.Db,
|
|
481
|
+
dispatchables: readonly ensureQueueEvents.Dispatchable[],
|
|
482
|
+
options: ensureQueueEvents.Options = {},
|
|
483
|
+
): Promise<ensureQueueEvents.Result> {
|
|
484
|
+
const now = (options.now ?? (() => new Date()))()
|
|
485
|
+
const expiresAt = new Date(now.getTime() + (options.ttl ?? deliveryRetentionMs)).toISOString()
|
|
486
|
+
const events = dispatchables.map(({ envelope, subscription }) => ({
|
|
487
|
+
createdAt: now.toISOString(),
|
|
488
|
+
envelope,
|
|
489
|
+
eventId: envelope.id,
|
|
490
|
+
// Inert while pending; the meaningful dedupe deadline is stamped at
|
|
491
|
+
// terminal transitions.
|
|
492
|
+
expiresAt,
|
|
493
|
+
// Due immediately; the value orders the sweep so it is never null here.
|
|
494
|
+
nextAttemptAt: now.toISOString(),
|
|
495
|
+
subscriptionId: subscription.id,
|
|
496
|
+
}))
|
|
497
|
+
return {
|
|
498
|
+
created: await WebhookQueueEvents.ensure(db, events),
|
|
499
|
+
references: events.map(({ eventId, subscriptionId }) => ({ eventId, subscriptionId })),
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export declare namespace ensureQueueEvents {
|
|
504
|
+
/** One full envelope and the subscription that produced it. */
|
|
505
|
+
type Dispatchable = {
|
|
506
|
+
/** Webhook envelope. */
|
|
507
|
+
envelope: Envelope
|
|
508
|
+
/** Subscription that owns the envelope. */
|
|
509
|
+
subscription: Subscription
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Staged-envelope retention options. */
|
|
513
|
+
type Options = {
|
|
514
|
+
/** Clock used for staging timestamps. */
|
|
515
|
+
now?: (() => Date) | undefined
|
|
516
|
+
/** Staged-envelope TTL in ms; defaults to {@link deliveryRetentionMs}. */
|
|
517
|
+
ttl?: number | undefined
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Created-row count plus references for every requested obligation. */
|
|
521
|
+
type Result = {
|
|
522
|
+
/** Newly staged obligations. */
|
|
523
|
+
created: number
|
|
524
|
+
/** Compact references for all inputs, including pre-existing rows. */
|
|
525
|
+
references: QueueReference[]
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** Claims one due pending obligation; misses classify why the claim is unavailable. */
|
|
530
|
+
export function claimQueueEvent(
|
|
531
|
+
db: Db.Db,
|
|
532
|
+
reference: QueueReference,
|
|
533
|
+
): Promise<WebhookQueueEvents.claim.Result> {
|
|
534
|
+
const now = new Date()
|
|
535
|
+
return WebhookQueueEvents.claim(db, reference, {
|
|
536
|
+
now: now.toISOString(),
|
|
537
|
+
staleBefore: new Date(now.getTime() - deliveryAttemptStaleMs).toISOString(),
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Marks a claimed obligation terminal and restarts its retention window, so
|
|
543
|
+
* the dedupe row outlives replays. Fenced by the claim marker: a stale
|
|
544
|
+
* claimant's completion returns false instead of clobbering a newer claim.
|
|
545
|
+
*/
|
|
546
|
+
export function completeQueueEvent(
|
|
547
|
+
db: Db.Db,
|
|
548
|
+
reference: QueueReference,
|
|
549
|
+
options: completeQueueEvent.Options,
|
|
550
|
+
): Promise<boolean> {
|
|
551
|
+
return WebhookQueueEvents.complete(db, reference, {
|
|
552
|
+
...options,
|
|
553
|
+
expiresAt: new Date(Date.now() + deliveryRetentionMs).toISOString(),
|
|
554
|
+
})
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export declare namespace completeQueueEvent {
|
|
558
|
+
/** Fenced terminal transition. */
|
|
559
|
+
type Options = {
|
|
560
|
+
/** Claim marker from the claimed record's `attemptingAt`. */
|
|
561
|
+
claimedAt: string
|
|
562
|
+
/** Terminal obligation outcome. */
|
|
563
|
+
status: WebhookQueueEvents.TerminalStatus
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Releases a claim and schedules the next attempt on the retry backoff.
|
|
569
|
+
* Fenced by the claim marker like {@link completeQueueEvent}.
|
|
570
|
+
*/
|
|
571
|
+
export function scheduleQueueEventRetry(
|
|
572
|
+
db: Db.Db,
|
|
573
|
+
reference: QueueReference,
|
|
574
|
+
options: scheduleQueueEventRetry.Options,
|
|
575
|
+
): Promise<boolean> {
|
|
576
|
+
return WebhookQueueEvents.schedule(db, reference, {
|
|
577
|
+
claimedAt: options.claimedAt,
|
|
578
|
+
nextAttemptAt: new Date(Date.now() + retryDelayMs(options.attemptCount)).toISOString(),
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export declare namespace scheduleQueueEventRetry {
|
|
583
|
+
/** Fenced retry scheduling. */
|
|
584
|
+
type Options = {
|
|
585
|
+
/** Attempts claimed so far; drives the backoff position. */
|
|
586
|
+
attemptCount: number
|
|
587
|
+
/** Claim marker from the claimed record's `attemptingAt`. */
|
|
588
|
+
claimedAt: string
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Lists due pending references for the sweeper, oldest first. */
|
|
593
|
+
export function dueQueueEvents(db: Db.Db, limit: number): Promise<QueueReference[]> {
|
|
594
|
+
const now = new Date()
|
|
595
|
+
return WebhookQueueEvents.due(db, {
|
|
596
|
+
limit,
|
|
597
|
+
now: now.toISOString(),
|
|
598
|
+
staleBefore: new Date(now.getTime() - deliveryAttemptStaleMs).toISOString(),
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Returns the authoritative pending-obligation snapshot. */
|
|
603
|
+
export function pendingQueueEvents(db: Db.Db): Promise<WebhookQueueEvents.pending.Result> {
|
|
604
|
+
const now = new Date()
|
|
605
|
+
return WebhookQueueEvents.pending(db, {
|
|
606
|
+
now: now.toISOString(),
|
|
607
|
+
staleBefore: new Date(now.getTime() - deliveryAttemptStaleMs).toISOString(),
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** Reads a full staged envelope for a compact Queue reference. */
|
|
612
|
+
export async function getQueueEvent(
|
|
613
|
+
db: Db.Db,
|
|
614
|
+
reference: QueueReference,
|
|
615
|
+
): Promise<QueueEvent | null> {
|
|
616
|
+
return WebhookQueueEvents.get(db, reference)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** Loads a staged envelope and delivers it with the subscription's current configuration. */
|
|
620
|
+
export async function deliverQueueEventAndRecord(
|
|
621
|
+
db: Db.Db,
|
|
622
|
+
reference: QueueReference,
|
|
623
|
+
options: deliverAndRecord.Options = {},
|
|
624
|
+
): Promise<deliverQueueEventAndRecord.Result> {
|
|
625
|
+
const dequeuedAt = Date.now()
|
|
626
|
+
const event = await getQueueEvent(db, reference)
|
|
627
|
+
if (!event) return { status: 'missing' }
|
|
628
|
+
return deliverClaimedAndRecord(db, event, { ...options, dequeuedAt })
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Delivers an already-loaded obligation envelope with the subscription's
|
|
633
|
+
* current configuration, skipping the staged-row read a claim already paid.
|
|
634
|
+
*/
|
|
635
|
+
export async function deliverClaimedAndRecord(
|
|
636
|
+
db: Db.Db,
|
|
637
|
+
claimed: deliverClaimedAndRecord.Claimed,
|
|
638
|
+
options: deliverClaimedAndRecord.Options = {},
|
|
639
|
+
): Promise<deliverClaimedAndRecord.Result> {
|
|
640
|
+
const dequeuedAt = options.dequeuedAt ?? Date.now()
|
|
641
|
+
const subscriptionReadStartedAt = Date.now()
|
|
642
|
+
const subscription = await WebhookSubscriptions.getById(
|
|
643
|
+
db,
|
|
644
|
+
claimed.subscriptionId,
|
|
645
|
+
new Date().toISOString(),
|
|
646
|
+
)
|
|
647
|
+
if (!subscription || subscription.status !== 'active') return { status: 'skipped' }
|
|
648
|
+
return {
|
|
649
|
+
result: await deliverAndRecord(db, subscription, claimed.envelope, {
|
|
650
|
+
...options,
|
|
651
|
+
dequeuedAt,
|
|
652
|
+
subscriptionReadMs: Date.now() - subscriptionReadStartedAt,
|
|
653
|
+
}),
|
|
654
|
+
status: 'delivered',
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export declare namespace deliverClaimedAndRecord {
|
|
659
|
+
/** Envelope and owning subscription id from a claimed obligation. */
|
|
660
|
+
type Claimed = {
|
|
661
|
+
/** Immutable event envelope to deliver. */
|
|
662
|
+
envelope: Envelope
|
|
663
|
+
/** Subscription re-resolved for current status and secret. */
|
|
664
|
+
subscriptionId: string
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Delivery options plus the dequeue timestamp of the claiming path. */
|
|
668
|
+
type Options = deliverAndRecord.Options & {
|
|
669
|
+
/** Epoch ms the claiming message was dequeued; defaults to now. */
|
|
670
|
+
dequeuedAt?: number | undefined
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/** A delivered event or an inactive subscription. */
|
|
674
|
+
type Result =
|
|
675
|
+
| {
|
|
676
|
+
/** Transport result from the current subscription state. */
|
|
677
|
+
result: WebhookDestination.Result
|
|
678
|
+
/** The envelope was delivered. */
|
|
679
|
+
status: 'delivered'
|
|
680
|
+
}
|
|
681
|
+
| {
|
|
682
|
+
/** The current subscription is inactive or deleted. */
|
|
683
|
+
status: 'skipped'
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export declare namespace deliverQueueEventAndRecord {
|
|
688
|
+
/** A delivered event, inactive subscription, or stale duplicate reference. */
|
|
689
|
+
type Result =
|
|
690
|
+
| {
|
|
691
|
+
/** Transport result from the current subscription state. */
|
|
692
|
+
result: WebhookDestination.Result
|
|
693
|
+
/** The staged event was delivered. */
|
|
694
|
+
status: 'delivered'
|
|
695
|
+
}
|
|
696
|
+
| {
|
|
697
|
+
/** The staged event or current subscription was not deliverable. */
|
|
698
|
+
status: 'missing' | 'skipped'
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
380
702
|
/** Records one delivery-log row (stateful, owner-agnostic; scoped per subscription). */
|
|
381
703
|
export async function recordDelivery(
|
|
382
704
|
db: Db.Db,
|
|
@@ -522,35 +844,31 @@ export async function deliverAndRecord(
|
|
|
522
844
|
envelope: Envelope,
|
|
523
845
|
options: deliverAndRecord.Options = {},
|
|
524
846
|
): Promise<WebhookDestination.Result> {
|
|
847
|
+
const startedAt = Date.now()
|
|
525
848
|
// Snapshot the attempt before recordSuccess/recordFailure mutate the counter.
|
|
526
|
-
const attempt = subscription.failureCount + 1
|
|
527
|
-
const result = await
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
849
|
+
const attempt = options.attempt ?? subscription.failureCount + 1
|
|
850
|
+
const result = await (async () => {
|
|
851
|
+
try {
|
|
852
|
+
return await WebhookDestination.from(subscription.destination).deliver({
|
|
853
|
+
envelope,
|
|
854
|
+
secret: subscription.secret,
|
|
855
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
856
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
857
|
+
})
|
|
858
|
+
} catch {
|
|
859
|
+
// A formatting failure is a failed attempt, not an exception that bypasses
|
|
860
|
+
// subscription failure state and delivery metrics on every queue retry.
|
|
861
|
+
return { error: 'delivery failed before request', ok: false }
|
|
862
|
+
}
|
|
863
|
+
})()
|
|
864
|
+
const stateWriteStartedAt = Date.now()
|
|
533
865
|
if (result.ok) await recordSuccess(db, subscription, options)
|
|
534
866
|
else await recordFailure(db, subscription, options)
|
|
535
|
-
|
|
536
|
-
// Emit a `webhook:delivery` metric. recordSuccess/recordFailure are immutable
|
|
537
|
-
// (they spread into a new record), so `subscription.failureCount` here is still
|
|
538
|
-
// the pre-mutation snapshot the sink needs to derive the attempt bucket.
|
|
539
|
-
// Best-effort, like delivery logging.
|
|
540
|
-
const metrics = options.metrics ? MetricSink.webhooks(options.metrics) : undefined
|
|
541
|
-
if (metrics) {
|
|
542
|
-
metrics.record({
|
|
543
|
-
envelope,
|
|
544
|
-
result,
|
|
545
|
-
subscription,
|
|
546
|
-
trigger: options.trigger ?? 'queue',
|
|
547
|
-
type: 'webhook:delivery',
|
|
548
|
-
})
|
|
549
|
-
metrics.flush()
|
|
550
|
-
}
|
|
867
|
+
const stateWriteMs = Date.now() - stateWriteStartedAt
|
|
551
868
|
|
|
552
869
|
// Append a delivery-log row. Best-effort: a logging failure must never change
|
|
553
870
|
// the delivery outcome the caller (Queue consumer / inline path) acts on.
|
|
871
|
+
const deliveryLogStartedAt = Date.now()
|
|
554
872
|
try {
|
|
555
873
|
await recordDelivery(
|
|
556
874
|
db,
|
|
@@ -560,15 +878,73 @@ export async function deliverAndRecord(
|
|
|
560
878
|
} catch {
|
|
561
879
|
// delivery logging is best-effort
|
|
562
880
|
}
|
|
881
|
+
const completedAt = Date.now()
|
|
882
|
+
|
|
883
|
+
// Emit after all delivery work so the timings cover every stage. The
|
|
884
|
+
// subscription is immutable, preserving the pre-attempt failure count.
|
|
885
|
+
const metrics = options.metrics ? MetricSink.webhooks(options.metrics) : undefined
|
|
886
|
+
if (metrics) {
|
|
887
|
+
const eventAt = eventTimestamp(envelope)
|
|
888
|
+
const envelopeAt = Date.parse(envelope.createdAt)
|
|
889
|
+
const dequeuedAt = options.dequeuedAt ?? startedAt
|
|
890
|
+
metrics.record({
|
|
891
|
+
envelope,
|
|
892
|
+
queueAttempt: options.queueAttempt,
|
|
893
|
+
result,
|
|
894
|
+
subscription,
|
|
895
|
+
attempt,
|
|
896
|
+
timings: {
|
|
897
|
+
deliveryLogMs: completedAt - deliveryLogStartedAt,
|
|
898
|
+
envelopeToQueueMs:
|
|
899
|
+
options.queuedAt === undefined || Number.isNaN(envelopeAt)
|
|
900
|
+
? undefined
|
|
901
|
+
: Math.max(0, options.queuedAt - envelopeAt),
|
|
902
|
+
endToEndMs: eventAt === undefined ? undefined : Math.max(0, completedAt - eventAt),
|
|
903
|
+
eventToEnvelopeMs:
|
|
904
|
+
eventAt === undefined || Number.isNaN(envelopeAt)
|
|
905
|
+
? undefined
|
|
906
|
+
: Math.max(0, envelopeAt - eventAt),
|
|
907
|
+
processingMs: Math.max(0, completedAt - dequeuedAt),
|
|
908
|
+
queueWaitMs:
|
|
909
|
+
options.queuedAt === undefined ? undefined : Math.max(0, dequeuedAt - options.queuedAt),
|
|
910
|
+
stateWriteMs,
|
|
911
|
+
subscriptionReadMs: options.subscriptionReadMs,
|
|
912
|
+
},
|
|
913
|
+
trigger: options.trigger ?? 'queue',
|
|
914
|
+
type: 'webhook:delivery',
|
|
915
|
+
})
|
|
916
|
+
metrics.flush()
|
|
917
|
+
}
|
|
563
918
|
|
|
564
919
|
return result
|
|
565
920
|
}
|
|
566
921
|
|
|
922
|
+
/** Reads an event timestamp from an envelope, falling back to its creation time. */
|
|
923
|
+
function eventTimestamp(envelope: Envelope): number | undefined {
|
|
924
|
+
const value =
|
|
925
|
+
typeof envelope.data === 'object' &&
|
|
926
|
+
envelope.data !== null &&
|
|
927
|
+
'timestamp' in envelope.data &&
|
|
928
|
+
typeof envelope.data.timestamp === 'string'
|
|
929
|
+
? envelope.data.timestamp
|
|
930
|
+
: envelope.createdAt
|
|
931
|
+
const timestamp = Date.parse(value)
|
|
932
|
+
return Number.isNaN(timestamp) ? undefined : timestamp
|
|
933
|
+
}
|
|
934
|
+
|
|
567
935
|
export declare namespace deliverAndRecord {
|
|
568
936
|
/** Options for {@link deliverAndRecord}. */
|
|
569
937
|
type Options = {
|
|
938
|
+
/**
|
|
939
|
+
* Delivery-log attempt number; the ledger claim count on the queue path.
|
|
940
|
+
* Defaults to the subscription failure counter, which concurrent
|
|
941
|
+
* deliveries can read stale.
|
|
942
|
+
*/
|
|
943
|
+
attempt?: number | undefined
|
|
570
944
|
/** Delivery-log row TTL in ms; defaults to {@link deliveryRetentionMs}. */
|
|
571
945
|
deliveryRetentionMs?: number | undefined
|
|
946
|
+
/** Queue dequeue time as epoch milliseconds, captured before subscription lookup. */
|
|
947
|
+
dequeuedAt?: number | undefined
|
|
572
948
|
/** `fetch` implementation passed through to {@link deliver}. */
|
|
573
949
|
fetch?: typeof globalThis.fetch | undefined
|
|
574
950
|
/** Consecutive failures before auto-disabling (default {@link maxFailures}). */
|
|
@@ -577,6 +953,12 @@ export declare namespace deliverAndRecord {
|
|
|
577
953
|
metrics?: Metrics.Metrics | undefined
|
|
578
954
|
/** Clock used for timestamps; injectable for tests. */
|
|
579
955
|
now?: (() => Date) | undefined
|
|
956
|
+
/** Cloudflare Queue delivery attempt, starting at one; absent for inline delivery paths. */
|
|
957
|
+
queueAttempt?: number | undefined
|
|
958
|
+
/** Queue enqueue time as epoch milliseconds, used to isolate queue wait. */
|
|
959
|
+
queuedAt?: number | undefined
|
|
960
|
+
/** Authoritative subscription lookup duration, supplied by the queue path. */
|
|
961
|
+
subscriptionReadMs?: number | undefined
|
|
580
962
|
/** Delivery timeout in ms passed through to {@link deliver}. */
|
|
581
963
|
timeoutMs?: number | undefined
|
|
582
964
|
/** Where this attempt originated; tags the `webhook:delivery` event (default `queue`). */
|
|
@@ -594,10 +976,16 @@ export async function deliverCurrentAndRecord(
|
|
|
594
976
|
envelope: Envelope,
|
|
595
977
|
options: deliverAndRecord.Options = {},
|
|
596
978
|
): Promise<deliverCurrentAndRecord.Result> {
|
|
979
|
+
const dequeuedAt = Date.now()
|
|
980
|
+
const subscriptionReadStartedAt = dequeuedAt
|
|
597
981
|
const subscription = await getSubscription(db, queued.owner, queued.id)
|
|
598
982
|
if (!subscription || subscription.status !== 'active') return { status: 'skipped' }
|
|
599
983
|
return {
|
|
600
|
-
result: await deliverAndRecord(db, subscription, envelope,
|
|
984
|
+
result: await deliverAndRecord(db, subscription, envelope, {
|
|
985
|
+
...options,
|
|
986
|
+
dequeuedAt,
|
|
987
|
+
subscriptionReadMs: Date.now() - subscriptionReadStartedAt,
|
|
988
|
+
}),
|
|
601
989
|
status: 'delivered',
|
|
602
990
|
}
|
|
603
991
|
}
|
|
@@ -13,7 +13,7 @@ const providers = [
|
|
|
13
13
|
integratorId: '0xdead',
|
|
14
14
|
userAddress: '0x1111111111111111111111111111111111111111',
|
|
15
15
|
}),
|
|
16
|
-
Relay.relay({
|
|
16
|
+
Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
|
|
17
17
|
Rhino.rhino(),
|
|
18
18
|
Squid.squid({
|
|
19
19
|
integratorId: 'squid-integrator-id',
|
|
@@ -286,6 +286,38 @@ describe('getQuotes', () => {
|
|
|
286
286
|
},
|
|
287
287
|
)
|
|
288
288
|
|
|
289
|
+
test('allows provider quotes within the default deadline', async () => {
|
|
290
|
+
vi.useFakeTimers()
|
|
291
|
+
try {
|
|
292
|
+
const quotes = FundingProvider.getQuotes({
|
|
293
|
+
...baseUsdcRequest,
|
|
294
|
+
providers: [
|
|
295
|
+
withGetQuote(
|
|
296
|
+
'relay',
|
|
297
|
+
() =>
|
|
298
|
+
new Promise((resolve) => {
|
|
299
|
+
setTimeout(
|
|
300
|
+
() =>
|
|
301
|
+
resolve({
|
|
302
|
+
destinationAmount: '990000',
|
|
303
|
+
sampledAt: '2026-01-01T00:00:00Z',
|
|
304
|
+
status: 'available',
|
|
305
|
+
}),
|
|
306
|
+
3_000,
|
|
307
|
+
)
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
],
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
await vi.advanceTimersByTimeAsync(3_000)
|
|
314
|
+
|
|
315
|
+
await expect(quotes).resolves.toHaveLength(1)
|
|
316
|
+
} finally {
|
|
317
|
+
vi.useRealTimers()
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
|
|
289
321
|
test('aborts providers that exceed the quote deadline', async () => {
|
|
290
322
|
const attempts: FundingProvider.ProviderAttempt[] = []
|
|
291
323
|
let aborted = false
|
|
@@ -730,26 +762,34 @@ describe('prepareTransfer', () => {
|
|
|
730
762
|
})
|
|
731
763
|
})
|
|
732
764
|
|
|
733
|
-
test('
|
|
765
|
+
test('enforces provider-style slippage rounding', async () => {
|
|
766
|
+
const inputs: FundingProvider.prepareTransfer.Parameters[] = []
|
|
734
767
|
const prepare = (destinationAmountMin: string) =>
|
|
735
768
|
FundingProvider.prepareTransfer({
|
|
736
769
|
...prepareOptions,
|
|
737
770
|
providers: [
|
|
738
|
-
withPrepareTransfer('relay', async () =>
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
771
|
+
withPrepareTransfer('relay', async (input) => {
|
|
772
|
+
inputs.push(input)
|
|
773
|
+
return {
|
|
774
|
+
...preparedResult(),
|
|
775
|
+
destinationAmount: '10001',
|
|
776
|
+
destinationAmountMin,
|
|
777
|
+
}
|
|
778
|
+
}),
|
|
743
779
|
],
|
|
744
780
|
slippageBps: 1,
|
|
745
781
|
})
|
|
746
782
|
|
|
747
|
-
await expect(prepare('
|
|
783
|
+
await expect(prepare('9998')).rejects.toMatchObject({
|
|
748
784
|
providerFailure: { failure: 'payload', id: 'relay', operation: 'prepareTransfer' },
|
|
749
785
|
})
|
|
786
|
+
await expect(prepare('9999')).resolves.toMatchObject({
|
|
787
|
+
result: { destinationAmount: '10001', destinationAmountMin: '9999' },
|
|
788
|
+
})
|
|
750
789
|
await expect(prepare('10000')).resolves.toMatchObject({
|
|
751
790
|
result: { destinationAmount: '10001', destinationAmountMin: '10000' },
|
|
752
791
|
})
|
|
792
|
+
expect(inputs.map((input) => input.slippageBps)).toEqual([1, 1, 1])
|
|
753
793
|
})
|
|
754
794
|
|
|
755
795
|
test.each([
|
|
@@ -250,6 +250,8 @@ export namespace schema {
|
|
|
250
250
|
destinationTransactionHashes: z.array(z.string()),
|
|
251
251
|
id: z.string().check(z.minLength(1)),
|
|
252
252
|
providerState: z.optional(z.record(z.string(), z.unknown())),
|
|
253
|
+
refundAmountExpected: z.optional(z.string().check(z.regex(/^\d+$/))),
|
|
254
|
+
refundTransactionHashes: z.array(z.string()),
|
|
253
255
|
sourceTransactionHashes: z.array(z.string()),
|
|
254
256
|
status: z.string().check(z.minLength(1)),
|
|
255
257
|
updatedAt: z.iso.datetime(),
|
|
@@ -773,7 +775,9 @@ export type ProviderFailure = {
|
|
|
773
775
|
provider: Provider
|
|
774
776
|
}
|
|
775
777
|
|
|
776
|
-
|
|
778
|
+
// Slow provider responses can still contain valid quotes; keep the boundary
|
|
779
|
+
// above observed Relay latency while bounding the total request duration.
|
|
780
|
+
const defaultQuoteTimeoutMs = 5_000
|
|
777
781
|
|
|
778
782
|
/** Returns normalized chains and source tokens supported by quote providers. */
|
|
779
783
|
export function getChains(options: getChains.Options) {
|
|
@@ -847,7 +851,7 @@ export async function getQuotes(options: getQuotes.Options) {
|
|
|
847
851
|
sourceChain: options.sourceChain,
|
|
848
852
|
sourceToken: options.sourceToken,
|
|
849
853
|
},
|
|
850
|
-
timeoutMs: options.providerTimeoutMs ??
|
|
854
|
+
timeoutMs: options.providerTimeoutMs ?? defaultQuoteTimeoutMs,
|
|
851
855
|
})
|
|
852
856
|
attempts.set(index, {
|
|
853
857
|
durationMs: performance.now() - start,
|
|
@@ -1123,13 +1127,14 @@ export async function prepareTransfer(
|
|
|
1123
1127
|
if (Date.parse(expiresAt) <= Date.now()) throw new ProviderPayloadError()
|
|
1124
1128
|
if (BigInt(result.destinationAmountMin) > BigInt(result.destinationAmount))
|
|
1125
1129
|
throw new ProviderPayloadError()
|
|
1126
|
-
//
|
|
1127
|
-
//
|
|
1130
|
+
// Providers may floor slippage-adjusted amounts to whole base units;
|
|
1131
|
+
// reject only minima below that allowed rounding.
|
|
1128
1132
|
if (options.slippageBps !== undefined) {
|
|
1129
1133
|
const basisPointScale = 10_000n
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1134
|
+
const minimumAllowed =
|
|
1135
|
+
(BigInt(result.destinationAmount) * BigInt(10_000 - options.slippageBps)) /
|
|
1136
|
+
basisPointScale
|
|
1137
|
+
if (BigInt(result.destinationAmountMin) < minimumAllowed) throw new ProviderPayloadError()
|
|
1133
1138
|
}
|
|
1134
1139
|
if (result.action.type === 'evm:calls')
|
|
1135
1140
|
try {
|
|
@@ -17,7 +17,6 @@ import * as Evidence from './Evidence.js'
|
|
|
17
17
|
import * as Reconciliation from './Reconciliation.js'
|
|
18
18
|
import * as Subsidy from './Subsidy.js'
|
|
19
19
|
|
|
20
|
-
const db = TestApp.database()
|
|
21
20
|
const runtime = Runtime.get()
|
|
22
21
|
const chain = {
|
|
23
22
|
addressFormat: 'hex',
|
|
@@ -28,6 +27,8 @@ const chain = {
|
|
|
28
27
|
|
|
29
28
|
describe.skipIf(runtime.mode !== 'localnet')('reconcileRequest', () => {
|
|
30
29
|
test('settles an underfilled deposit through the native DEX', async () => {
|
|
30
|
+
// Vitest retries reuse module state, so each attempt needs a fresh database.
|
|
31
|
+
const db = TestApp.database()
|
|
31
32
|
const destinationTokenAddress = runtime.fixtures?.crossTokenTransfer?.destinationToken
|
|
32
33
|
if (!destinationTokenAddress) throw new Error('Expected a seeded local DEX pair.')
|
|
33
34
|
const rpcUrl = runtime.rpcUrl
|
|
@@ -120,6 +121,7 @@ describe.skipIf(runtime.mode !== 'localnet')('reconcileRequest', () => {
|
|
|
120
121
|
createdAt: new Date().toISOString(),
|
|
121
122
|
destinationTransactionHashes: [destinationReceipt.receipt.transactionHash],
|
|
122
123
|
id: 'local-deposit-request',
|
|
124
|
+
refundTransactionHashes: [],
|
|
123
125
|
sourceTransactionHashes: [sourceReceipt.receipt.transactionHash],
|
|
124
126
|
status: 'success',
|
|
125
127
|
updatedAt: new Date().toISOString(),
|