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.
Files changed (319) hide show
  1. package/dist/ApiKey.d.ts +2 -1
  2. package/dist/ApiKey.d.ts.map +1 -1
  3. package/dist/ApiKeys.d.ts +4 -2
  4. package/dist/ApiKeys.d.ts.map +1 -1
  5. package/dist/ApiKeys.js +26 -1
  6. package/dist/ApiKeys.js.map +1 -1
  7. package/dist/App.d.ts +4292 -3708
  8. package/dist/App.d.ts.map +1 -1
  9. package/dist/App.js +1 -0
  10. package/dist/App.js.map +1 -1
  11. package/dist/Webhooks.d.ts +21 -20
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +66 -23
  14. package/dist/Webhooks.js.map +1 -1
  15. package/dist/admin/App.d.ts +91 -84
  16. package/dist/admin/App.d.ts.map +1 -1
  17. package/dist/admin/apps/api-keys.d.ts +26 -17
  18. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  19. package/dist/admin/apps/api-keys.js +6 -0
  20. package/dist/admin/apps/api-keys.js.map +1 -1
  21. package/dist/admin/apps/organizations.d.ts +4 -2
  22. package/dist/admin/apps/organizations.d.ts.map +1 -1
  23. package/dist/admin/apps/verified-tokens.d.ts +168 -168
  24. package/dist/admin/ui.generated.d.ts +1 -1
  25. package/dist/admin/ui.generated.js +69 -69
  26. package/dist/admin/ui.generated.js.map +1 -1
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +26 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/Zones.d.ts +4280 -84
  31. package/dist/apps/Zones.d.ts.map +1 -1
  32. package/dist/apps/Zones.js +30 -10
  33. package/dist/apps/Zones.js.map +1 -1
  34. package/dist/apps/data/App.d.ts +5799 -4929
  35. package/dist/apps/data/App.d.ts.map +1 -1
  36. package/dist/apps/data/FxOracle.d.ts +4 -5
  37. package/dist/apps/data/FxOracle.d.ts.map +1 -1
  38. package/dist/apps/data/FxOracle.js +19 -4
  39. package/dist/apps/data/FxOracle.js.map +1 -1
  40. package/dist/apps/data/routes/activities.d.ts +3391 -492
  41. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  42. package/dist/apps/data/routes/activities.js +372 -29
  43. package/dist/apps/data/routes/activities.js.map +1 -1
  44. package/dist/apps/data/routes/addresses.d.ts +853 -339
  45. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  46. package/dist/apps/data/routes/balances.d.ts +10 -10
  47. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  48. package/dist/apps/data/routes/balances.js +145 -83
  49. package/dist/apps/data/routes/balances.js.map +1 -1
  50. package/dist/apps/data/routes/blocks.d.ts +123 -123
  51. package/dist/apps/data/routes/coingecko.d.ts +169 -169
  52. package/dist/apps/data/routes/earn.d.ts +455 -1
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +369 -45
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  57. package/dist/apps/data/routes/exchanges.js +114 -118
  58. package/dist/apps/data/routes/exchanges.js.map +1 -1
  59. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  60. package/dist/apps/data/routes/indexer.d.ts +35 -34
  61. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  62. package/dist/apps/data/routes/indexer.js +3 -2
  63. package/dist/apps/data/routes/indexer.js.map +1 -1
  64. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  65. package/dist/apps/data/routes/receipts.js +68 -47
  66. package/dist/apps/data/routes/receipts.js.map +1 -1
  67. package/dist/apps/data/routes/rpc.d.ts +31 -31
  68. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  69. package/dist/apps/data/routes/rpc.js +5 -1
  70. package/dist/apps/data/routes/rpc.js.map +1 -1
  71. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  72. package/dist/apps/data/routes/tokens.d.ts +461 -459
  73. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  74. package/dist/apps/data/routes/transactions.d.ts +313 -305
  75. package/dist/apps/data/routes/transactions.d.ts.map +1 -1
  76. package/dist/apps/data/routes/transactions.js +25 -23
  77. package/dist/apps/data/routes/transactions.js.map +1 -1
  78. package/dist/apps/data/routes/transfers.d.ts +108 -86
  79. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  80. package/dist/apps/data/routes/transfers.js +618 -105
  81. package/dist/apps/data/routes/transfers.js.map +1 -1
  82. package/dist/apps/data/routes/valuation.d.ts +21 -21
  83. package/dist/apps/data/routes/valuation.d.ts.map +1 -1
  84. package/dist/apps/data/routes/valuation.js +2 -2
  85. package/dist/apps/data/routes/valuation.js.map +1 -1
  86. package/dist/apps/data/routes/webhooks.d.ts +519 -868
  87. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  88. package/dist/apps/data/routes/webhooks.js +642 -353
  89. package/dist/apps/data/routes/webhooks.js.map +1 -1
  90. package/dist/apps/data/routes/zones.d.ts +31 -31
  91. package/dist/apps/data/routes/zones.d.ts.map +1 -1
  92. package/dist/apps/data/routes/zones.js +4 -2
  93. package/dist/apps/data/routes/zones.js.map +1 -1
  94. package/dist/apps/funding/routes/deposit-addresses.js +1 -1
  95. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  96. package/dist/apps/funding/routes/providers.d.ts +13 -13
  97. package/dist/apps/funding/routes/quotes.d.ts +89 -89
  98. package/dist/apps/funding/routes/transfers.d.ts +36 -36
  99. package/dist/apps/funding/routes/transfers.js +1 -1
  100. package/dist/apps/funding/routes/transfers.js.map +1 -1
  101. package/dist/apps/management/App.d.ts +11 -309
  102. package/dist/apps/management/App.d.ts.map +1 -1
  103. package/dist/apps/management/routes/api-keys.d.ts +74 -74
  104. package/dist/apps/management/routes/billing.js +1 -1
  105. package/dist/apps/management/routes/billing.js.map +1 -1
  106. package/dist/apps/management/routes/faucet.d.ts +35 -35
  107. package/dist/apps/management/routes/faucet.js +2 -2
  108. package/dist/apps/management/routes/faucet.js.map +1 -1
  109. package/dist/apps/management/routes/invitations.d.ts +134 -134
  110. package/dist/apps/management/routes/invite-links.d.ts +80 -80
  111. package/dist/apps/management/routes/members.d.ts +69 -69
  112. package/dist/apps/management/routes/orgs.d.ts +98 -98
  113. package/dist/apps/management/routes/projects.d.ts +60 -60
  114. package/dist/apps/management/routes/usage.d.ts +72 -72
  115. package/dist/apps/management/routes/verified-token-requests.d.ts +83 -83
  116. package/dist/apps/management/routes/webhooks.d.ts +203 -618
  117. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  118. package/dist/apps/mcp.d.ts.map +1 -1
  119. package/dist/apps/mcp.js +4 -1
  120. package/dist/apps/mcp.js.map +1 -1
  121. package/dist/apps/mpp/App.d.ts +56 -44
  122. package/dist/apps/mpp/App.d.ts.map +1 -1
  123. package/dist/apps/mpp/Mpp.d.ts +3 -0
  124. package/dist/apps/mpp/Mpp.d.ts.map +1 -1
  125. package/dist/apps/mpp/Mpp.js +3 -0
  126. package/dist/apps/mpp/Mpp.js.map +1 -1
  127. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  128. package/dist/apps/mpp/internal/Adapter.js +9 -2
  129. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  130. package/dist/db/Db.d.ts +4 -2
  131. package/dist/db/Db.d.ts.map +1 -1
  132. package/dist/db/Db.js +75 -15
  133. package/dist/db/Db.js.map +1 -1
  134. package/dist/db/Schema.d.ts +19 -0
  135. package/dist/db/Schema.d.ts.map +1 -1
  136. package/dist/db/Schema.js +24 -0
  137. package/dist/db/Schema.js.map +1 -1
  138. package/dist/db/tables/sponsoredTransactions.d.ts.map +1 -1
  139. package/dist/db/tables/sponsoredTransactions.js +4 -1
  140. package/dist/db/tables/sponsoredTransactions.js.map +1 -1
  141. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  142. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  143. package/dist/db/tables/webhookQueueEvents.js +160 -0
  144. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  145. package/dist/db/tables/webhookSubscriptions.d.ts +58 -3
  146. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  147. package/dist/db/tables/webhookSubscriptions.js +86 -3
  148. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  149. package/dist/internal/Auth.d.ts +3 -0
  150. package/dist/internal/Auth.d.ts.map +1 -1
  151. package/dist/internal/Auth.js +6 -4
  152. package/dist/internal/Auth.js.map +1 -1
  153. package/dist/internal/MetricSink.d.ts +139 -3
  154. package/dist/internal/MetricSink.d.ts.map +1 -1
  155. package/dist/internal/MetricSink.js +144 -4
  156. package/dist/internal/MetricSink.js.map +1 -1
  157. package/dist/internal/RateLimit.d.ts +4 -6
  158. package/dist/internal/RateLimit.d.ts.map +1 -1
  159. package/dist/internal/RateLimit.js +15 -14
  160. package/dist/internal/RateLimit.js.map +1 -1
  161. package/dist/internal/Schema.js +1 -1
  162. package/dist/internal/Schema.js.map +1 -1
  163. package/dist/internal/Store.d.ts +2 -1
  164. package/dist/internal/Store.d.ts.map +1 -1
  165. package/dist/internal/Store.js +57 -18
  166. package/dist/internal/Store.js.map +1 -1
  167. package/dist/internal/Tidx.d.ts +1 -3
  168. package/dist/internal/Tidx.d.ts.map +1 -1
  169. package/dist/internal/Tidx.js +1 -3
  170. package/dist/internal/Tidx.js.map +1 -1
  171. package/dist/internal/Value.d.ts +2 -0
  172. package/dist/internal/Value.d.ts.map +1 -1
  173. package/dist/internal/Value.js +25 -0
  174. package/dist/internal/Value.js.map +1 -1
  175. package/dist/internal/Viem.d.ts.map +1 -1
  176. package/dist/internal/Viem.js +2 -1
  177. package/dist/internal/Viem.js.map +1 -1
  178. package/dist/internal/WebhookDestination.d.ts +2 -0
  179. package/dist/internal/WebhookDestination.d.ts.map +1 -1
  180. package/dist/internal/WebhookDestination.js +11 -0
  181. package/dist/internal/WebhookDestination.js.map +1 -1
  182. package/dist/internal/WebhookTransfer.d.ts +75 -0
  183. package/dist/internal/WebhookTransfer.d.ts.map +1 -0
  184. package/dist/internal/WebhookTransfer.js +299 -0
  185. package/dist/internal/WebhookTransfer.js.map +1 -0
  186. package/dist/internal/Webhooks.d.ts +176 -0
  187. package/dist/internal/Webhooks.d.ts.map +1 -1
  188. package/dist/internal/Webhooks.js +216 -20
  189. package/dist/internal/Webhooks.js.map +1 -1
  190. package/dist/internal/funding/Provider.d.ts +4 -0
  191. package/dist/internal/funding/Provider.d.ts.map +1 -1
  192. package/dist/internal/funding/Provider.js +11 -7
  193. package/dist/internal/funding/Provider.js.map +1 -1
  194. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  195. package/dist/internal/funding/Reconciliation.js +128 -0
  196. package/dist/internal/funding/Reconciliation.js.map +1 -1
  197. package/dist/internal/funding/providers/relay.d.ts +6 -4
  198. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  199. package/dist/internal/funding/providers/relay.js +71 -9
  200. package/dist/internal/funding/providers/relay.js.map +1 -1
  201. package/package.json +4 -4
  202. package/src/ApiKeys.test.ts +20 -0
  203. package/src/ApiKeys.ts +28 -1
  204. package/src/App.test.ts +10 -8
  205. package/src/App.ts +1 -0
  206. package/src/Client.test-d.ts +61 -5
  207. package/src/Webhooks.bench.ts +714 -0
  208. package/src/Webhooks.memory.bench.ts +250 -0
  209. package/src/Webhooks.test.ts +77 -6
  210. package/src/Webhooks.ts +85 -29
  211. package/src/admin/apps/api-keys.test.ts +9 -0
  212. package/src/admin/apps/api-keys.ts +15 -0
  213. package/src/admin/ui/src/lib/query-client.ts +3 -2
  214. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  215. package/src/admin/ui.generated.ts +69 -69
  216. package/src/analytics/Analytics.test.ts +8 -0
  217. package/src/analytics/Analytics.ts +26 -0
  218. package/src/apps/Zones.test.ts +23 -5
  219. package/src/apps/Zones.ts +31 -10
  220. package/src/apps/composition.test.ts +1 -0
  221. package/src/apps/data/App.test.ts +2 -2
  222. package/src/apps/data/FxOracle.test.ts +18 -0
  223. package/src/apps/data/FxOracle.ts +28 -5
  224. package/src/apps/data/routes/activities.bench.ts +41 -0
  225. package/src/apps/data/routes/activities.test.ts +122 -2
  226. package/src/apps/data/routes/activities.ts +435 -32
  227. package/src/apps/data/routes/balances.bench.ts +21 -0
  228. package/src/apps/data/routes/balances.test.ts +119 -10
  229. package/src/apps/data/routes/balances.ts +164 -97
  230. package/src/apps/data/routes/blocks.bench.ts +34 -0
  231. package/src/apps/data/routes/blocks.test.ts +12 -6
  232. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  233. package/src/apps/data/routes/earn.bench.ts +71 -0
  234. package/src/apps/data/routes/earn.test.ts +197 -0
  235. package/src/apps/data/routes/earn.ts +487 -45
  236. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  237. package/src/apps/data/routes/exchanges.test.ts +7 -6
  238. package/src/apps/data/routes/exchanges.ts +124 -117
  239. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  240. package/src/apps/data/routes/indexer.bench.ts +8 -0
  241. package/src/apps/data/routes/indexer.test.ts +2 -1
  242. package/src/apps/data/routes/indexer.ts +3 -2
  243. package/src/apps/data/routes/receipts.bench.ts +39 -0
  244. package/src/apps/data/routes/receipts.test.ts +24 -7
  245. package/src/apps/data/routes/receipts.ts +67 -50
  246. package/src/apps/data/routes/rpc.bench.ts +14 -0
  247. package/src/apps/data/routes/rpc.test.ts +27 -0
  248. package/src/apps/data/routes/rpc.ts +5 -1
  249. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  250. package/src/apps/data/routes/tokens.bench.ts +76 -0
  251. package/src/apps/data/routes/tokens.test.ts +51 -0
  252. package/src/apps/data/routes/transactions.bench.ts +40 -0
  253. package/src/apps/data/routes/transactions.test.ts +42 -0
  254. package/src/apps/data/routes/transactions.ts +44 -39
  255. package/src/apps/data/routes/transfers.bench.ts +28 -0
  256. package/src/apps/data/routes/transfers.test.ts +402 -24
  257. package/src/apps/data/routes/transfers.ts +708 -101
  258. package/src/apps/data/routes/valuation.bench.ts +14 -0
  259. package/src/apps/data/routes/valuation.test.ts +26 -0
  260. package/src/apps/data/routes/valuation.ts +6 -2
  261. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  262. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  263. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  264. package/src/apps/data/routes/webhooks.test.ts +466 -135
  265. package/src/apps/data/routes/webhooks.ts +820 -429
  266. package/src/apps/data/routes/zones.bench.ts +32 -0
  267. package/src/apps/data/routes/zones.test.ts +1 -1
  268. package/src/apps/data/routes/zones.ts +7 -2
  269. package/src/apps/funding/routes/chains.test.ts +1 -1
  270. package/src/apps/funding/routes/deposit-addresses.test.ts +1 -1
  271. package/src/apps/funding/routes/deposit-addresses.ts +1 -1
  272. package/src/apps/funding/routes/providers.test.ts +1 -1
  273. package/src/apps/funding/routes/quotes.test.ts +2 -2
  274. package/src/apps/funding/routes/transfers.test.ts +3 -6
  275. package/src/apps/funding/routes/transfers.ts +1 -1
  276. package/src/apps/management/routes/billing.ts +1 -1
  277. package/src/apps/management/routes/faucet.ts +2 -2
  278. package/src/apps/management/routes/me.test.ts +31 -0
  279. package/src/apps/mcp.ts +4 -1
  280. package/src/apps/mpp/Mpp.test.ts +13 -1
  281. package/src/apps/mpp/Mpp.ts +8 -0
  282. package/src/apps/mpp/internal/Adapter.ts +10 -2
  283. package/src/db/Db.test.ts +40 -6
  284. package/src/db/Db.ts +114 -19
  285. package/src/db/Schema.ts +32 -0
  286. package/src/db/tables/sponsoredTransactions.ts +9 -2
  287. package/src/db/tables/webhookQueueEvents.ts +285 -0
  288. package/src/db/tables/webhookSubscriptions.ts +131 -4
  289. package/src/internal/Auth.test.ts +61 -50
  290. package/src/internal/Auth.ts +9 -4
  291. package/src/internal/EarnVaults.test.ts +2 -2
  292. package/src/internal/MetricSink.test.ts +575 -0
  293. package/src/internal/MetricSink.ts +325 -10
  294. package/src/internal/RateLimit.test.ts +23 -18
  295. package/src/internal/RateLimit.ts +23 -15
  296. package/src/internal/Schema.ts +1 -1
  297. package/src/internal/Store.test.ts +99 -13
  298. package/src/internal/Store.ts +52 -17
  299. package/src/internal/Tidx.test.ts +4 -4
  300. package/src/internal/Tidx.ts +1 -3
  301. package/src/internal/Value.test.ts +25 -0
  302. package/src/internal/Value.ts +29 -0
  303. package/src/internal/Viem.test.ts +8 -0
  304. package/src/internal/Viem.ts +2 -1
  305. package/src/internal/WebhookDestination.test.ts +20 -0
  306. package/src/internal/WebhookDestination.ts +10 -0
  307. package/src/internal/WebhookTransfer.test.ts +89 -0
  308. package/src/internal/WebhookTransfer.ts +430 -0
  309. package/src/internal/Webhooks.test.ts +616 -0
  310. package/src/internal/Webhooks.ts +412 -24
  311. package/src/internal/funding/Provider.test.ts +48 -8
  312. package/src/internal/funding/Provider.ts +12 -7
  313. package/src/internal/funding/Reconciliation.local.test.ts +3 -1
  314. package/src/internal/funding/Reconciliation.test.ts +1240 -0
  315. package/src/internal/funding/Reconciliation.ts +136 -0
  316. package/src/internal/funding/SourceObservation.test.ts +1 -0
  317. package/src/internal/funding/providers/index.test.ts +73 -3
  318. package/src/internal/funding/providers/relay.test.ts +2 -9
  319. package/src/internal/funding/providers/relay.ts +72 -13
@@ -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 WebhookDestination.from(subscription.destination).deliver({
528
- envelope,
529
- secret: subscription.secret,
530
- ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
531
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
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, options),
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({ userAddress: '0x1111111111111111111111111111111111111111' }),
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('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(),