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
package/src/internal/Webhooks.ts
CHANGED
|
@@ -326,9 +326,59 @@ export async function listActive(
|
|
|
326
326
|
chainId: number,
|
|
327
327
|
eventType: EventType,
|
|
328
328
|
): Promise<Subscription[]> {
|
|
329
|
+
const pairs = await listActiveWithCursors(db, chainId, eventType)
|
|
330
|
+
return pairs.map((pair) => pair.subscription)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Lists a stream's live active subscriptions paired with their poller cursors,
|
|
335
|
+
* from one query. The poller's per-tick lookup.
|
|
336
|
+
*/
|
|
337
|
+
export async function listActiveWithCursors(
|
|
338
|
+
db: Db.Db,
|
|
339
|
+
chainId: number,
|
|
340
|
+
eventType: EventType,
|
|
341
|
+
): Promise<{ cursor: string | null; subscription: Subscription }[]> {
|
|
329
342
|
return WebhookSubscriptions.listActive(db, chainId, eventType, new Date().toISOString())
|
|
330
343
|
}
|
|
331
344
|
|
|
345
|
+
/** Reads every live active subscription on a chain in one poller query. */
|
|
346
|
+
export async function listActiveForChain(
|
|
347
|
+
db: Db.Db,
|
|
348
|
+
options: listActiveForChain.Options,
|
|
349
|
+
): Promise<{ cursor: string | null; subscription: Subscription }[]> {
|
|
350
|
+
return WebhookSubscriptions.listActiveForChain(db, {
|
|
351
|
+
chainId: options.chainId,
|
|
352
|
+
eventTypes: options.eventTypes,
|
|
353
|
+
now: new Date().toISOString(),
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export declare namespace listActiveForChain {
|
|
358
|
+
/** Filters for the poller's chain-level subscription read. */
|
|
359
|
+
type Options = Pick<WebhookSubscriptions.listActiveForChain.Options, 'chainId' | 'eventTypes'>
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Reads the freshest cursor block across a chain's active subscriptions, or
|
|
364
|
+
* null when none carry a cursor. Creation checkpoints count at their recorded
|
|
365
|
+
* head, so a brand-new subscription reads as fresh.
|
|
366
|
+
*/
|
|
367
|
+
export async function maxCursorBlock(db: Db.Db, chainId: number): Promise<number | null> {
|
|
368
|
+
const cursors = await WebhookSubscriptions.listActiveCursors(
|
|
369
|
+
db,
|
|
370
|
+
chainId,
|
|
371
|
+
new Date().toISOString(),
|
|
372
|
+
)
|
|
373
|
+
let max: number | null = null
|
|
374
|
+
for (const cursor of cursors) {
|
|
375
|
+
const decoded = Cursor.decode(cursor, ['int', 'int']) ?? Cursor.decode(cursor, ['int'])
|
|
376
|
+
const block = decoded?.[0]
|
|
377
|
+
if (typeof block === 'number' && (max === null || block > max)) max = block
|
|
378
|
+
}
|
|
379
|
+
return max
|
|
380
|
+
}
|
|
381
|
+
|
|
332
382
|
/** Reads a subscription's keyset cursor. Returns null when unset. */
|
|
333
383
|
export async function getCursor(db: Db.Db, subscriptionId: string): Promise<string | null> {
|
|
334
384
|
return WebhookSubscriptions.getCursor(db, subscriptionId)
|
|
@@ -347,6 +397,19 @@ export declare namespace compareAndSetCursor {
|
|
|
347
397
|
type Options = WebhookSubscriptions.compareAndSetCursor.Options
|
|
348
398
|
}
|
|
349
399
|
|
|
400
|
+
/** Writes cursors whose stored values match their expected cursors. */
|
|
401
|
+
export async function compareAndSetCursors(
|
|
402
|
+
db: Db.Db,
|
|
403
|
+
options: compareAndSetCursors.Options,
|
|
404
|
+
): Promise<Set<string>> {
|
|
405
|
+
return WebhookSubscriptions.compareAndSetCursors(db, options)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export declare namespace compareAndSetCursors {
|
|
409
|
+
/** Options for {@link compareAndSetCursors}. */
|
|
410
|
+
type Options = WebhookSubscriptions.compareAndSetCursors.Options
|
|
411
|
+
}
|
|
412
|
+
|
|
350
413
|
/** Writes a subscription's keyset cursor. */
|
|
351
414
|
export async function setCursor(db: Db.Db, subscriptionId: string, cursor: string): Promise<void> {
|
|
352
415
|
await WebhookSubscriptions.setCursor(db, subscriptionId, cursor)
|
|
@@ -522,6 +585,7 @@ export async function deliverAndRecord(
|
|
|
522
585
|
envelope: Envelope,
|
|
523
586
|
options: deliverAndRecord.Options = {},
|
|
524
587
|
): Promise<WebhookDestination.Result> {
|
|
588
|
+
const startedAt = Date.now()
|
|
525
589
|
// Snapshot the attempt before recordSuccess/recordFailure mutate the counter.
|
|
526
590
|
const attempt = subscription.failureCount + 1
|
|
527
591
|
const result = await WebhookDestination.from(subscription.destination).deliver({
|
|
@@ -530,27 +594,14 @@ export async function deliverAndRecord(
|
|
|
530
594
|
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
531
595
|
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
532
596
|
})
|
|
597
|
+
const stateWriteStartedAt = Date.now()
|
|
533
598
|
if (result.ok) await recordSuccess(db, subscription, options)
|
|
534
599
|
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
|
-
}
|
|
600
|
+
const stateWriteMs = Date.now() - stateWriteStartedAt
|
|
551
601
|
|
|
552
602
|
// Append a delivery-log row. Best-effort: a logging failure must never change
|
|
553
603
|
// the delivery outcome the caller (Queue consumer / inline path) acts on.
|
|
604
|
+
const deliveryLogStartedAt = Date.now()
|
|
554
605
|
try {
|
|
555
606
|
await recordDelivery(
|
|
556
607
|
db,
|
|
@@ -560,15 +611,66 @@ export async function deliverAndRecord(
|
|
|
560
611
|
} catch {
|
|
561
612
|
// delivery logging is best-effort
|
|
562
613
|
}
|
|
614
|
+
const completedAt = Date.now()
|
|
615
|
+
|
|
616
|
+
// Emit after all delivery work so the timings cover every stage. The
|
|
617
|
+
// subscription is immutable, preserving the pre-attempt failure count.
|
|
618
|
+
const metrics = options.metrics ? MetricSink.webhooks(options.metrics) : undefined
|
|
619
|
+
if (metrics) {
|
|
620
|
+
const eventAt = eventTimestamp(envelope)
|
|
621
|
+
const envelopeAt = Date.parse(envelope.createdAt)
|
|
622
|
+
const dequeuedAt = options.dequeuedAt ?? startedAt
|
|
623
|
+
metrics.record({
|
|
624
|
+
envelope,
|
|
625
|
+
queueAttempt: options.queueAttempt,
|
|
626
|
+
result,
|
|
627
|
+
subscription,
|
|
628
|
+
timings: {
|
|
629
|
+
deliveryLogMs: completedAt - deliveryLogStartedAt,
|
|
630
|
+
envelopeToQueueMs:
|
|
631
|
+
options.queuedAt === undefined || Number.isNaN(envelopeAt)
|
|
632
|
+
? undefined
|
|
633
|
+
: Math.max(0, options.queuedAt - envelopeAt),
|
|
634
|
+
endToEndMs: eventAt === undefined ? undefined : Math.max(0, completedAt - eventAt),
|
|
635
|
+
eventToEnvelopeMs:
|
|
636
|
+
eventAt === undefined || Number.isNaN(envelopeAt)
|
|
637
|
+
? undefined
|
|
638
|
+
: Math.max(0, envelopeAt - eventAt),
|
|
639
|
+
processingMs: Math.max(0, completedAt - dequeuedAt),
|
|
640
|
+
queueWaitMs:
|
|
641
|
+
options.queuedAt === undefined ? undefined : Math.max(0, dequeuedAt - options.queuedAt),
|
|
642
|
+
stateWriteMs,
|
|
643
|
+
subscriptionReadMs: options.subscriptionReadMs,
|
|
644
|
+
},
|
|
645
|
+
trigger: options.trigger ?? 'queue',
|
|
646
|
+
type: 'webhook:delivery',
|
|
647
|
+
})
|
|
648
|
+
metrics.flush()
|
|
649
|
+
}
|
|
563
650
|
|
|
564
651
|
return result
|
|
565
652
|
}
|
|
566
653
|
|
|
654
|
+
/** Reads an event timestamp from an envelope, falling back to its creation time. */
|
|
655
|
+
function eventTimestamp(envelope: Envelope): number | undefined {
|
|
656
|
+
const value =
|
|
657
|
+
typeof envelope.data === 'object' &&
|
|
658
|
+
envelope.data !== null &&
|
|
659
|
+
'timestamp' in envelope.data &&
|
|
660
|
+
typeof envelope.data.timestamp === 'string'
|
|
661
|
+
? envelope.data.timestamp
|
|
662
|
+
: envelope.createdAt
|
|
663
|
+
const timestamp = Date.parse(value)
|
|
664
|
+
return Number.isNaN(timestamp) ? undefined : timestamp
|
|
665
|
+
}
|
|
666
|
+
|
|
567
667
|
export declare namespace deliverAndRecord {
|
|
568
668
|
/** Options for {@link deliverAndRecord}. */
|
|
569
669
|
type Options = {
|
|
570
670
|
/** Delivery-log row TTL in ms; defaults to {@link deliveryRetentionMs}. */
|
|
571
671
|
deliveryRetentionMs?: number | undefined
|
|
672
|
+
/** Queue dequeue time as epoch milliseconds, captured before subscription lookup. */
|
|
673
|
+
dequeuedAt?: number | undefined
|
|
572
674
|
/** `fetch` implementation passed through to {@link deliver}. */
|
|
573
675
|
fetch?: typeof globalThis.fetch | undefined
|
|
574
676
|
/** Consecutive failures before auto-disabling (default {@link maxFailures}). */
|
|
@@ -577,6 +679,12 @@ export declare namespace deliverAndRecord {
|
|
|
577
679
|
metrics?: Metrics.Metrics | undefined
|
|
578
680
|
/** Clock used for timestamps; injectable for tests. */
|
|
579
681
|
now?: (() => Date) | undefined
|
|
682
|
+
/** Cloudflare Queue delivery attempt, starting at one; absent for inline delivery paths. */
|
|
683
|
+
queueAttempt?: number | undefined
|
|
684
|
+
/** Queue enqueue time as epoch milliseconds, used to isolate queue wait. */
|
|
685
|
+
queuedAt?: number | undefined
|
|
686
|
+
/** Authoritative subscription lookup duration, supplied by the queue path. */
|
|
687
|
+
subscriptionReadMs?: number | undefined
|
|
580
688
|
/** Delivery timeout in ms passed through to {@link deliver}. */
|
|
581
689
|
timeoutMs?: number | undefined
|
|
582
690
|
/** Where this attempt originated; tags the `webhook:delivery` event (default `queue`). */
|
|
@@ -594,10 +702,16 @@ export async function deliverCurrentAndRecord(
|
|
|
594
702
|
envelope: Envelope,
|
|
595
703
|
options: deliverAndRecord.Options = {},
|
|
596
704
|
): Promise<deliverCurrentAndRecord.Result> {
|
|
705
|
+
const dequeuedAt = Date.now()
|
|
706
|
+
const subscriptionReadStartedAt = dequeuedAt
|
|
597
707
|
const subscription = await getSubscription(db, queued.owner, queued.id)
|
|
598
708
|
if (!subscription || subscription.status !== 'active') return { status: 'skipped' }
|
|
599
709
|
return {
|
|
600
|
-
result: await deliverAndRecord(db, subscription, envelope,
|
|
710
|
+
result: await deliverAndRecord(db, subscription, envelope, {
|
|
711
|
+
...options,
|
|
712
|
+
dequeuedAt,
|
|
713
|
+
subscriptionReadMs: Date.now() - subscriptionReadStartedAt,
|
|
714
|
+
}),
|
|
601
715
|
status: 'delivered',
|
|
602
716
|
}
|
|
603
717
|
}
|
|
@@ -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(),
|