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.
Files changed (198) hide show
  1. package/dist/App.d.ts +4887 -5074
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/Webhooks.d.ts +21 -20
  4. package/dist/Webhooks.d.ts.map +1 -1
  5. package/dist/Webhooks.js +66 -23
  6. package/dist/Webhooks.js.map +1 -1
  7. package/dist/admin/apps/verified-tokens.d.ts +168 -168
  8. package/dist/apps/Zones.d.ts +34 -22
  9. package/dist/apps/Zones.d.ts.map +1 -1
  10. package/dist/apps/Zones.js +9 -8
  11. package/dist/apps/Zones.js.map +1 -1
  12. package/dist/apps/data/App.d.ts +5198 -5099
  13. package/dist/apps/data/App.d.ts.map +1 -1
  14. package/dist/apps/data/FxOracle.d.ts +4 -5
  15. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  16. package/dist/apps/data/FxOracle.js +19 -4
  17. package/dist/apps/data/FxOracle.js.map +1 -1
  18. package/dist/apps/data/routes/activities.d.ts +270 -270
  19. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  20. package/dist/apps/data/routes/activities.js +32 -27
  21. package/dist/apps/data/routes/activities.js.map +1 -1
  22. package/dist/apps/data/routes/addresses.d.ts +52 -52
  23. package/dist/apps/data/routes/balances.d.ts +36 -36
  24. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  25. package/dist/apps/data/routes/balances.js +135 -81
  26. package/dist/apps/data/routes/balances.js.map +1 -1
  27. package/dist/apps/data/routes/blocks.d.ts +125 -125
  28. package/dist/apps/data/routes/coingecko.d.ts +54 -54
  29. package/dist/apps/data/routes/earn.d.ts +512 -58
  30. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  31. package/dist/apps/data/routes/earn.js +368 -44
  32. package/dist/apps/data/routes/earn.js.map +1 -1
  33. package/dist/apps/data/routes/exchanges.d.ts +120 -120
  34. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  35. package/dist/apps/data/routes/exchanges.js +114 -118
  36. package/dist/apps/data/routes/exchanges.js.map +1 -1
  37. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  38. package/dist/apps/data/routes/receipts.js +68 -47
  39. package/dist/apps/data/routes/receipts.js.map +1 -1
  40. package/dist/apps/data/routes/rpc.d.ts +31 -31
  41. package/dist/apps/data/routes/tokens.d.ts +461 -459
  42. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  43. package/dist/apps/data/routes/transactions.d.ts +59 -51
  44. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  45. package/dist/apps/data/routes/transactions.js +25 -23
  46. package/dist/apps/data/routes/transactions.js.map +1 -1
  47. package/dist/apps/data/routes/transfers.d.ts +108 -86
  48. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  49. package/dist/apps/data/routes/transfers.js +618 -105
  50. package/dist/apps/data/routes/transfers.js.map +1 -1
  51. package/dist/apps/data/routes/valuation.d.ts +21 -21
  52. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  53. package/dist/apps/data/routes/valuation.js +2 -2
  54. package/dist/apps/data/routes/valuation.js.map +1 -1
  55. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  56. package/dist/apps/data/routes/webhooks.d.ts +102 -505
  57. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  58. package/dist/apps/data/routes/webhooks.js +484 -342
  59. package/dist/apps/data/routes/webhooks.js.map +1 -1
  60. package/dist/apps/data/routes/zones.d.ts +12 -12
  61. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  62. package/dist/apps/data/routes/zones.js +3 -1
  63. package/dist/apps/data/routes/zones.js.map +1 -1
  64. package/dist/apps/funding/App.d.ts +96 -96
  65. package/dist/apps/funding/routes/deposit-addresses.d.ts +52 -52
  66. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  67. package/dist/apps/funding/routes/transfers.d.ts +4 -4
  68. package/dist/apps/management/App.d.ts +429 -727
  69. package/dist/apps/management/App.d.ts.map +1 -1
  70. package/dist/apps/management/routes/api-keys.d.ts +24 -24
  71. package/dist/apps/management/routes/billing.d.ts +72 -72
  72. package/dist/apps/management/routes/faucet.d.ts +6 -6
  73. package/dist/apps/management/routes/invitations.d.ts +134 -134
  74. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  75. package/dist/apps/management/routes/me.d.ts +6 -6
  76. package/dist/apps/management/routes/members.d.ts +30 -30
  77. package/dist/apps/management/routes/orgs.d.ts +98 -98
  78. package/dist/apps/management/routes/projects.d.ts +20 -20
  79. package/dist/apps/management/routes/usage.d.ts +24 -24
  80. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  81. package/dist/apps/management/routes/webhooks.d.ts +158 -573
  82. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/mpp/App.d.ts +56 -44
  84. package/dist/apps/mpp/App.d.ts.map +1 -1
  85. package/dist/apps/mpp/Mpp.d.ts +3 -0
  86. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  87. package/dist/apps/mpp/Mpp.js +3 -0
  88. package/dist/apps/mpp/Mpp.js.map +1 -1
  89. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  90. package/dist/apps/mpp/internal/Adapter.js +9 -2
  91. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  92. package/dist/db/Db.d.ts +2 -2
  93. package/dist/db/Db.d.ts.map +1 -1
  94. package/dist/db/Db.js +17 -15
  95. package/dist/db/Db.js.map +1 -1
  96. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  97. package/dist/db/tables/sponsoredTransactions.js +4 -1
  98. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  99. package/dist/db/tables/webhookSubscriptions.d.ts +56 -3
  100. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  101. package/dist/db/tables/webhookSubscriptions.js +76 -3
  102. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  103. package/dist/internal/MetricSink.d.ts +112 -3
  104. package/dist/internal/MetricSink.d.ts.map +1 -1
  105. package/dist/internal/MetricSink.js +116 -3
  106. package/dist/internal/MetricSink.js.map +1 -1
  107. package/dist/internal/Schema.js +1 -1
  108. package/dist/internal/Schema.js.map +1 -1
  109. package/dist/internal/Tidx.d.ts +1 -3
  110. package/dist/internal/Tidx.d.ts.map +1 -1
  111. package/dist/internal/Tidx.js +1 -3
  112. package/dist/internal/Tidx.js.map +1 -1
  113. package/dist/internal/Viem.d.ts.map +1 -1
  114. package/dist/internal/Viem.js +2 -1
  115. package/dist/internal/Viem.js.map +1 -1
  116. package/dist/internal/WebhookDestination.d.ts +2 -0
  117. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  118. package/dist/internal/WebhookDestination.js +11 -0
  119. package/dist/internal/WebhookDestination.js.map +1 -1
  120. package/dist/internal/WebhookTransfer.d.ts +55 -0
  121. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  122. package/dist/internal/WebhookTransfer.js +296 -0
  123. package/dist/internal/WebhookTransfer.js.map +1 -0
  124. package/dist/internal/Webhooks.d.ts +37 -0
  125. package/dist/internal/Webhooks.d.ts.map +1 -1
  126. package/dist/internal/Webhooks.js +87 -13
  127. package/dist/internal/Webhooks.js.map +1 -1
  128. package/dist/internal/funding/Provider.d.ts +4 -0
  129. package/dist/internal/funding/Provider.d.ts.map +1 -1
  130. package/dist/internal/funding/Provider.js +11 -7
  131. package/dist/internal/funding/Provider.js.map +1 -1
  132. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  133. package/dist/internal/funding/Reconciliation.js +128 -0
  134. package/dist/internal/funding/Reconciliation.js.map +1 -1
  135. package/dist/internal/funding/providers/relay.d.ts +3 -0
  136. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  137. package/dist/internal/funding/providers/relay.js +12 -1
  138. package/dist/internal/funding/providers/relay.js.map +1 -1
  139. package/package.json +3 -3
  140. package/src/Client.test-d.ts +60 -5
  141. package/src/Webhooks.bench.ts +714 -0
  142. package/src/Webhooks.memory.bench.ts +250 -0
  143. package/src/Webhooks.test.ts +77 -6
  144. package/src/Webhooks.ts +83 -29
  145. package/src/apps/Zones.test.ts +5 -4
  146. package/src/apps/Zones.ts +9 -8
  147. package/src/apps/data/FxOracle.test.ts +18 -0
  148. package/src/apps/data/FxOracle.ts +28 -5
  149. package/src/apps/data/routes/activities.test.ts +19 -0
  150. package/src/apps/data/routes/activities.ts +34 -30
  151. package/src/apps/data/routes/balances.test.ts +119 -10
  152. package/src/apps/data/routes/balances.ts +154 -95
  153. package/src/apps/data/routes/blocks.test.ts +12 -6
  154. package/src/apps/data/routes/earn.test.ts +197 -0
  155. package/src/apps/data/routes/earn.ts +486 -44
  156. package/src/apps/data/routes/exchanges.test.ts +7 -6
  157. package/src/apps/data/routes/exchanges.ts +124 -117
  158. package/src/apps/data/routes/receipts.test.ts +24 -7
  159. package/src/apps/data/routes/receipts.ts +67 -50
  160. package/src/apps/data/routes/tokens.test.ts +51 -0
  161. package/src/apps/data/routes/transactions.test.ts +42 -0
  162. package/src/apps/data/routes/transactions.ts +44 -39
  163. package/src/apps/data/routes/transfers.test.ts +402 -24
  164. package/src/apps/data/routes/transfers.ts +708 -101
  165. package/src/apps/data/routes/valuation.test.ts +26 -0
  166. package/src/apps/data/routes/valuation.ts +6 -2
  167. package/src/apps/data/routes/webhooks.test.ts +414 -130
  168. package/src/apps/data/routes/webhooks.ts +603 -419
  169. package/src/apps/data/routes/zones.ts +6 -1
  170. package/src/apps/funding/routes/quotes.test.ts +1 -1
  171. package/src/apps/mpp/Mpp.test.ts +13 -1
  172. package/src/apps/mpp/Mpp.ts +8 -0
  173. package/src/apps/mpp/internal/Adapter.ts +10 -2
  174. package/src/db/Db.test.ts +40 -6
  175. package/src/db/Db.ts +33 -19
  176. package/src/db/tables/sponsoredTransactions.ts +9 -2
  177. package/src/db/tables/webhookSubscriptions.ts +116 -4
  178. package/src/internal/EarnVaults.test.ts +2 -2
  179. package/src/internal/MetricSink.test.ts +518 -0
  180. package/src/internal/MetricSink.ts +266 -9
  181. package/src/internal/Schema.ts +1 -1
  182. package/src/internal/Tidx.test.ts +4 -4
  183. package/src/internal/Tidx.ts +1 -3
  184. package/src/internal/Viem.test.ts +8 -0
  185. package/src/internal/Viem.ts +2 -1
  186. package/src/internal/WebhookDestination.test.ts +20 -0
  187. package/src/internal/WebhookDestination.ts +10 -0
  188. package/src/internal/WebhookTransfer.ts +426 -0
  189. package/src/internal/Webhooks.test.ts +137 -0
  190. package/src/internal/Webhooks.ts +131 -17
  191. package/src/internal/funding/Provider.test.ts +47 -7
  192. package/src/internal/funding/Provider.ts +12 -7
  193. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  194. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  195. package/src/internal/funding/Reconciliation.ts +136 -0
  196. package/src/internal/funding/SourceObservation.test.ts +1 -0
  197. package/src/internal/funding/providers/relay.test.ts +1 -4
  198. package/src/internal/funding/providers/relay.ts +16 -1
@@ -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, options),
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('rounds the slippage floor upward', async () => {
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
- ...preparedResult(),
740
- destinationAmount: '10001',
741
- destinationAmountMin,
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('9999')).rejects.toMatchObject({
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
- const defaultTimeoutMs = 2_000
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 ?? defaultTimeoutMs,
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
- // A provider that ignores the caller's slippage bound is unusable for
1127
- // this request; enforce the bound against the quoted minimum.
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 numerator = BigInt(result.destinationAmount) * BigInt(10_000 - options.slippageBps)
1131
- const floor = (numerator + basisPointScale - 1n) / basisPointScale
1132
- if (BigInt(result.destinationAmountMin) < floor) throw new ProviderPayloadError()
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(),