tapimo 0.10.0 → 0.10.2

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 (295) 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 +6007 -5003
  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 +1 -1
  12. package/dist/Webhooks.d.ts.map +1 -1
  13. package/dist/Webhooks.js +6 -6
  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 +22 -13
  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/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +69 -69
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts.map +1 -1
  27. package/dist/analytics/Analytics.js +232 -0
  28. package/dist/analytics/Analytics.js.map +1 -1
  29. package/dist/analytics/tables/requestEvents.d.ts +4 -0
  30. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  31. package/dist/analytics/tables/requestEvents.js +2 -0
  32. package/dist/analytics/tables/requestEvents.js.map +1 -1
  33. package/dist/apps/Zones.d.ts +4266 -82
  34. package/dist/apps/Zones.d.ts.map +1 -1
  35. package/dist/apps/Zones.js +27 -8
  36. package/dist/apps/Zones.js.map +1 -1
  37. package/dist/apps/data/App.d.ts +3467 -2696
  38. package/dist/apps/data/App.d.ts.map +1 -1
  39. package/dist/apps/data/App.js +2 -0
  40. package/dist/apps/data/App.js.map +1 -1
  41. package/dist/apps/data/routes/activities.d.ts +2999 -100
  42. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  43. package/dist/apps/data/routes/activities.js +340 -2
  44. package/dist/apps/data/routes/activities.js.map +1 -1
  45. package/dist/apps/data/routes/addresses.d.ts +514 -0
  46. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  47. package/dist/apps/data/routes/balances.d.ts +26 -26
  48. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  49. package/dist/apps/data/routes/balances.js +7 -3
  50. package/dist/apps/data/routes/balances.js.map +1 -1
  51. package/dist/apps/data/routes/blocks.d.ts +4 -4
  52. package/dist/apps/data/routes/earn.d.ts +75 -75
  53. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  54. package/dist/apps/data/routes/earn.js +65 -36
  55. package/dist/apps/data/routes/earn.js.map +1 -1
  56. package/dist/apps/data/routes/exchanges.d.ts +124 -120
  57. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  58. package/dist/apps/data/routes/exchanges.js +115 -77
  59. package/dist/apps/data/routes/exchanges.js.map +1 -1
  60. package/dist/apps/data/routes/fee-amm.d.ts +24 -24
  61. package/dist/apps/data/routes/indexer.d.ts +2 -1
  62. package/dist/apps/data/routes/indexer.d.ts.map +1 -1
  63. package/dist/apps/data/routes/indexer.js +3 -2
  64. package/dist/apps/data/routes/indexer.js.map +1 -1
  65. package/dist/apps/data/routes/receipts.d.ts +4 -3
  66. package/dist/apps/data/routes/receipts.d.ts.map +1 -1
  67. package/dist/apps/data/routes/receipts.js +15 -14
  68. package/dist/apps/data/routes/receipts.js.map +1 -1
  69. package/dist/apps/data/routes/rpc.d.ts.map +1 -1
  70. package/dist/apps/data/routes/rpc.js +5 -1
  71. package/dist/apps/data/routes/rpc.js.map +1 -1
  72. package/dist/apps/data/routes/tokenlist.d.ts +12 -12
  73. package/dist/apps/data/routes/tokens.d.ts +1 -1
  74. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  75. package/dist/apps/data/routes/tokens.js +4 -3
  76. package/dist/apps/data/routes/tokens.js.map +1 -1
  77. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  78. package/dist/apps/data/routes/transfers.js +25 -7
  79. package/dist/apps/data/routes/transfers.js.map +1 -1
  80. package/dist/apps/data/routes/verified-tokens.d.ts +37 -37
  81. package/dist/apps/data/routes/webhooks.d.ts +71 -1
  82. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  83. package/dist/apps/data/routes/webhooks.js +177 -23
  84. package/dist/apps/data/routes/webhooks.js.map +1 -1
  85. package/dist/apps/data/routes/zones.js +1 -1
  86. package/dist/apps/data/routes/zones.js.map +1 -1
  87. package/dist/apps/funding/App.d.ts +322 -89
  88. package/dist/apps/funding/App.d.ts.map +1 -1
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts +375 -45
  90. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  91. package/dist/apps/funding/routes/deposit-addresses.js +208 -12
  92. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  93. package/dist/apps/funding/routes/transfers.d.ts +32 -32
  94. package/dist/apps/funding/routes/transfers.js +1 -1
  95. package/dist/apps/funding/routes/transfers.js.map +1 -1
  96. package/dist/apps/management/App.d.ts +418 -418
  97. package/dist/apps/management/routes/api-keys.d.ts +50 -50
  98. package/dist/apps/management/routes/billing.d.ts +72 -72
  99. package/dist/apps/management/routes/billing.js +1 -1
  100. package/dist/apps/management/routes/billing.js.map +1 -1
  101. package/dist/apps/management/routes/faucet.d.ts +8 -8
  102. package/dist/apps/management/routes/faucet.js +2 -2
  103. package/dist/apps/management/routes/faucet.js.map +1 -1
  104. package/dist/apps/management/routes/invite-links.d.ts +42 -42
  105. package/dist/apps/management/routes/me.d.ts +6 -6
  106. package/dist/apps/management/routes/members.d.ts +14 -14
  107. package/dist/apps/management/routes/projects.d.ts +40 -40
  108. package/dist/apps/management/routes/usage.d.ts +8 -8
  109. package/dist/apps/management/routes/webhooks.d.ts +49 -49
  110. package/dist/apps/mcp.d.ts.map +1 -1
  111. package/dist/apps/mcp.js +4 -1
  112. package/dist/apps/mcp.js.map +1 -1
  113. package/dist/db/Db.d.ts +2 -0
  114. package/dist/db/Db.d.ts.map +1 -1
  115. package/dist/db/Db.js +90 -0
  116. package/dist/db/Db.js.map +1 -1
  117. package/dist/db/Schema.d.ts +19 -0
  118. package/dist/db/Schema.d.ts.map +1 -1
  119. package/dist/db/Schema.js +24 -0
  120. package/dist/db/Schema.js.map +1 -1
  121. package/dist/db/tables/fundingDepositAddresses.d.ts +20 -0
  122. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  123. package/dist/db/tables/fundingDepositAddresses.js +17 -0
  124. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  125. package/dist/db/tables/fundingDeposits.d.ts +41 -0
  126. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  127. package/dist/db/tables/fundingDeposits.js +69 -0
  128. package/dist/db/tables/fundingDeposits.js.map +1 -1
  129. package/dist/db/tables/webhookQueueEvents.d.ts +122 -0
  130. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -0
  131. package/dist/db/tables/webhookQueueEvents.js +160 -0
  132. package/dist/db/tables/webhookQueueEvents.js.map +1 -0
  133. package/dist/db/tables/webhookSubscriptions.d.ts +2 -0
  134. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  135. package/dist/db/tables/webhookSubscriptions.js +10 -0
  136. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  137. package/dist/internal/Auth.d.ts +3 -26
  138. package/dist/internal/Auth.d.ts.map +1 -1
  139. package/dist/internal/Auth.js +6 -37
  140. package/dist/internal/Auth.js.map +1 -1
  141. package/dist/internal/Credential.d.ts +27 -0
  142. package/dist/internal/Credential.d.ts.map +1 -0
  143. package/dist/internal/Credential.js +34 -0
  144. package/dist/internal/Credential.js.map +1 -0
  145. package/dist/internal/EarnRates.js +11 -7
  146. package/dist/internal/EarnRates.js.map +1 -1
  147. package/dist/internal/Log.d.ts +8 -0
  148. package/dist/internal/Log.d.ts.map +1 -1
  149. package/dist/internal/Log.js +4 -0
  150. package/dist/internal/Log.js.map +1 -1
  151. package/dist/internal/MetricSink.d.ts +51 -0
  152. package/dist/internal/MetricSink.d.ts.map +1 -1
  153. package/dist/internal/MetricSink.js +54 -2
  154. package/dist/internal/MetricSink.js.map +1 -1
  155. package/dist/internal/RateLimit.d.ts +4 -6
  156. package/dist/internal/RateLimit.d.ts.map +1 -1
  157. package/dist/internal/RateLimit.js +15 -14
  158. package/dist/internal/RateLimit.js.map +1 -1
  159. package/dist/internal/Store.d.ts +2 -1
  160. package/dist/internal/Store.d.ts.map +1 -1
  161. package/dist/internal/Store.js +57 -18
  162. package/dist/internal/Store.js.map +1 -1
  163. package/dist/internal/Tidx.d.ts +4 -3
  164. package/dist/internal/Tidx.d.ts.map +1 -1
  165. package/dist/internal/Tidx.js +2 -2
  166. package/dist/internal/Tidx.js.map +1 -1
  167. package/dist/internal/Value.d.ts +2 -0
  168. package/dist/internal/Value.d.ts.map +1 -1
  169. package/dist/internal/Value.js +25 -0
  170. package/dist/internal/Value.js.map +1 -1
  171. package/dist/internal/Viem.d.ts +4 -3
  172. package/dist/internal/Viem.d.ts.map +1 -1
  173. package/dist/internal/Viem.js +3 -2
  174. package/dist/internal/Viem.js.map +1 -1
  175. package/dist/internal/WebhookTransfer.d.ts +20 -0
  176. package/dist/internal/WebhookTransfer.d.ts.map +1 -1
  177. package/dist/internal/WebhookTransfer.js +7 -4
  178. package/dist/internal/WebhookTransfer.js.map +1 -1
  179. package/dist/internal/Webhooks.d.ts +141 -0
  180. package/dist/internal/Webhooks.d.ts.map +1 -1
  181. package/dist/internal/Webhooks.js +139 -10
  182. package/dist/internal/Webhooks.js.map +1 -1
  183. package/dist/internal/funding/providers/relay.d.ts +3 -4
  184. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  185. package/dist/internal/funding/providers/relay.js +59 -8
  186. package/dist/internal/funding/providers/relay.js.map +1 -1
  187. package/dist/internal/index.d.ts +1 -0
  188. package/dist/internal/index.d.ts.map +1 -1
  189. package/dist/internal/index.js +1 -0
  190. package/dist/internal/index.js.map +1 -1
  191. package/package.json +3 -3
  192. package/src/ApiKeys.test.ts +20 -0
  193. package/src/ApiKeys.ts +28 -1
  194. package/src/App.test.ts +40 -8
  195. package/src/App.ts +1 -0
  196. package/src/Client.test-d.ts +34 -1
  197. package/src/Webhooks.ts +20 -18
  198. package/src/admin/apps/api-keys.test.ts +9 -0
  199. package/src/admin/apps/api-keys.ts +15 -0
  200. package/src/admin/ui/src/lib/query-client.ts +3 -2
  201. package/src/admin/ui/src/routes/api-keys.tsx +31 -9
  202. package/src/admin/ui.generated.ts +69 -69
  203. package/src/analytics/Analytics.test.ts +42 -0
  204. package/src/analytics/Analytics.ts +232 -0
  205. package/src/analytics/tables/requestEvents.ts +6 -0
  206. package/src/apps/Zones.test.ts +20 -3
  207. package/src/apps/Zones.ts +28 -8
  208. package/src/apps/composition.test.ts +2 -0
  209. package/src/apps/data/App.test.ts +2 -2
  210. package/src/apps/data/App.ts +2 -0
  211. package/src/apps/data/routes/activities.bench.ts +41 -0
  212. package/src/apps/data/routes/activities.test.ts +103 -2
  213. package/src/apps/data/routes/activities.ts +401 -2
  214. package/src/apps/data/routes/balances.bench.ts +21 -0
  215. package/src/apps/data/routes/balances.ts +7 -3
  216. package/src/apps/data/routes/blocks.bench.ts +34 -0
  217. package/src/apps/data/routes/coingecko.bench.ts +53 -0
  218. package/src/apps/data/routes/earn.bench.ts +71 -0
  219. package/src/apps/data/routes/earn.ts +66 -36
  220. package/src/apps/data/routes/exchanges.bench.ts +85 -0
  221. package/src/apps/data/routes/exchanges.test.ts +8 -3
  222. package/src/apps/data/routes/exchanges.ts +134 -82
  223. package/src/apps/data/routes/fee-amm.bench.ts +13 -0
  224. package/src/apps/data/routes/indexer.bench.ts +8 -0
  225. package/src/apps/data/routes/indexer.test.ts +2 -1
  226. package/src/apps/data/routes/indexer.ts +3 -2
  227. package/src/apps/data/routes/receipts.bench.ts +39 -0
  228. package/src/apps/data/routes/receipts.ts +16 -14
  229. package/src/apps/data/routes/rpc.bench.ts +14 -0
  230. package/src/apps/data/routes/rpc.test.ts +27 -0
  231. package/src/apps/data/routes/rpc.ts +5 -1
  232. package/src/apps/data/routes/tokenlist.bench.ts +3 -0
  233. package/src/apps/data/routes/tokens.bench.ts +76 -0
  234. package/src/apps/data/routes/tokens.ts +3 -2
  235. package/src/apps/data/routes/transactions.bench.ts +48 -0
  236. package/src/apps/data/routes/transfers.bench.ts +28 -0
  237. package/src/apps/data/routes/transfers.ts +36 -10
  238. package/src/apps/data/routes/valuation.bench.ts +14 -0
  239. package/src/apps/data/routes/verified-tokens.bench.ts +8 -0
  240. package/src/apps/data/routes/webhooks.bench.ts +26 -0
  241. package/src/apps/data/routes/webhooks.scan.test.ts +326 -0
  242. package/src/apps/data/routes/webhooks.test.ts +53 -6
  243. package/src/apps/data/routes/webhooks.ts +252 -24
  244. package/src/apps/data/routes/zones.bench.ts +32 -0
  245. package/src/apps/data/routes/zones.test.ts +1 -1
  246. package/src/apps/data/routes/zones.ts +1 -1
  247. package/src/apps/funding/routes/chains.test.ts +1 -1
  248. package/src/apps/funding/routes/deposit-addresses.test.ts +217 -3
  249. package/src/apps/funding/routes/deposit-addresses.ts +255 -19
  250. package/src/apps/funding/routes/providers.test.ts +1 -1
  251. package/src/apps/funding/routes/quotes.test.ts +1 -1
  252. package/src/apps/funding/routes/transfers.test.ts +20 -8
  253. package/src/apps/funding/routes/transfers.ts +1 -1
  254. package/src/apps/management/routes/billing.ts +1 -1
  255. package/src/apps/management/routes/faucet.ts +2 -2
  256. package/src/apps/management/routes/me.test.ts +31 -0
  257. package/src/apps/mcp.ts +4 -1
  258. package/src/db/Db.ts +117 -0
  259. package/src/db/Schema.ts +32 -0
  260. package/src/db/tables/fundingDepositAddresses.test.ts +28 -0
  261. package/src/db/tables/fundingDepositAddresses.ts +39 -0
  262. package/src/db/tables/fundingDeposits.test.ts +82 -0
  263. package/src/db/tables/fundingDeposits.ts +130 -0
  264. package/src/db/tables/webhookQueueEvents.ts +285 -0
  265. package/src/db/tables/webhookSubscriptions.ts +15 -0
  266. package/src/internal/Auth.test.ts +61 -98
  267. package/src/internal/Auth.ts +9 -63
  268. package/src/internal/Credential.test.ts +51 -0
  269. package/src/internal/Credential.ts +58 -0
  270. package/src/internal/EarnRates.ts +13 -7
  271. package/src/internal/EarnVaults.test.ts +2 -2
  272. package/src/internal/Log.test.ts +3 -1
  273. package/src/internal/Log.ts +14 -0
  274. package/src/internal/MetricSink.test.ts +108 -2
  275. package/src/internal/MetricSink.ts +112 -2
  276. package/src/internal/RateLimit.test.ts +23 -18
  277. package/src/internal/RateLimit.ts +23 -15
  278. package/src/internal/Store.test.ts +99 -13
  279. package/src/internal/Store.ts +52 -17
  280. package/src/internal/Tidx.test.ts +4 -4
  281. package/src/internal/Tidx.ts +5 -4
  282. package/src/internal/Value.test.ts +25 -0
  283. package/src/internal/Value.ts +29 -0
  284. package/src/internal/Viem.test.ts +4 -0
  285. package/src/internal/Viem.ts +6 -4
  286. package/src/internal/WebhookTransfer.test.ts +89 -0
  287. package/src/internal/WebhookTransfer.ts +11 -7
  288. package/src/internal/Webhooks.test.ts +488 -0
  289. package/src/internal/Webhooks.ts +291 -10
  290. package/src/internal/funding/Provider.test.ts +1 -1
  291. package/src/internal/funding/providers/index.test.ts +73 -3
  292. package/src/internal/funding/providers/relay.test.ts +1 -5
  293. package/src/internal/funding/providers/relay.ts +56 -12
  294. package/src/internal/index.test.ts +1 -0
  295. package/src/internal/index.ts +1 -0
@@ -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'
@@ -372,13 +373,20 @@ export async function maxCursorBlock(db: Db.Db, chainId: number): Promise<number
372
373
  )
373
374
  let max: number | null = null
374
375
  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
376
+ const block = cursorBlock(cursor)
377
+ if (block !== undefined && (max === null || block > max)) max = block
378
378
  }
379
379
  return max
380
380
  }
381
381
 
382
+ /** Decodes the block component of a subscription cursor, if any. */
383
+ export function cursorBlock(cursor: string | null): number | undefined {
384
+ if (cursor === null) return undefined
385
+ const decoded = Cursor.decode(cursor, ['int', 'int']) ?? Cursor.decode(cursor, ['int'])
386
+ const block = decoded?.[0]
387
+ return typeof block === 'number' ? block : undefined
388
+ }
389
+
382
390
  /** Reads a subscription's keyset cursor. Returns null when unset. */
383
391
  export async function getCursor(db: Db.Db, subscriptionId: string): Promise<string | null> {
384
392
  return WebhookSubscriptions.getCursor(db, subscriptionId)
@@ -423,6 +431,7 @@ export async function setCursor(db: Db.Db, subscriptionId: string, cursor: strin
423
431
  export async function pruneExpired(db: Db.Db, options: pruneExpired.Options = {}): Promise<void> {
424
432
  const iso = (options.now ?? (() => new Date()))().toISOString()
425
433
  await WebhookDeliveries.pruneExpired(db, iso)
434
+ await WebhookQueueEvents.pruneExpired(db, iso)
426
435
  await WebhookSubscriptions.pruneExpired(db, iso)
427
436
  }
428
437
 
@@ -440,6 +449,263 @@ export declare namespace pruneExpired {
440
449
  */
441
450
  export const deliveryRetentionMs = 7 * 24 * 60 * 60 * 1_000
442
451
 
452
+ /** A compact Cloudflare Queue message referencing a durable staged envelope. */
453
+ export type QueueReference = {
454
+ /** Idempotent event id of the staged envelope. */
455
+ eventId: string
456
+ /** Subscription that owns the staged envelope. */
457
+ subscriptionId: string
458
+ }
459
+
460
+ /** A full envelope staged outside Cloudflare Queues. */
461
+ export type QueueEvent = QueueReference & {
462
+ /** ISO timestamp when the envelope was staged. */
463
+ createdAt: string
464
+ /** Full envelope loaded by the Queue consumer. */
465
+ envelope: Envelope
466
+ /** ISO timestamp after which an unconsumed envelope may be pruned. */
467
+ expiresAt: string
468
+ }
469
+
470
+ /** A claim newer than this counts as an in-flight attempt; above the 10s destination timeout default. */
471
+ export const deliveryAttemptStaleMs = 60_000
472
+
473
+ /**
474
+ * Retry backoff by attempt count: 5s, 25s, ~2m, ~10m, ~52m, then hourly.
475
+ * Endpoint outages retry on this schedule until delivery or auto-disable.
476
+ */
477
+ export function retryDelayMs(attemptCount: number): number {
478
+ return Math.min(5_000 * 5 ** Math.max(0, attemptCount - 1), 3_600_000)
479
+ }
480
+
481
+ /**
482
+ * Stages full envelopes as delivery obligations and returns compact
483
+ * references safe for Queue admission. Existing rows are left untouched, so
484
+ * the first staged envelope wins and terminal outcomes never resurrect.
485
+ */
486
+ export async function ensureQueueEvents(
487
+ db: Db.Db,
488
+ dispatchables: readonly ensureQueueEvents.Dispatchable[],
489
+ options: ensureQueueEvents.Options = {},
490
+ ): Promise<ensureQueueEvents.Result> {
491
+ const now = (options.now ?? (() => new Date()))()
492
+ const expiresAt = new Date(now.getTime() + (options.ttl ?? deliveryRetentionMs)).toISOString()
493
+ const events = dispatchables.map(({ envelope, subscription }) => ({
494
+ createdAt: now.toISOString(),
495
+ envelope,
496
+ eventId: envelope.id,
497
+ // Inert while pending; the meaningful dedupe deadline is stamped at
498
+ // terminal transitions.
499
+ expiresAt,
500
+ // Due immediately; the value orders the sweep so it is never null here.
501
+ nextAttemptAt: now.toISOString(),
502
+ subscriptionId: subscription.id,
503
+ }))
504
+ return {
505
+ created: await WebhookQueueEvents.ensure(db, events),
506
+ references: events.map(({ eventId, subscriptionId }) => ({ eventId, subscriptionId })),
507
+ }
508
+ }
509
+
510
+ export declare namespace ensureQueueEvents {
511
+ /** One full envelope and the subscription that produced it. */
512
+ type Dispatchable = {
513
+ /** Webhook envelope. */
514
+ envelope: Envelope
515
+ /** Subscription that owns the envelope. */
516
+ subscription: Subscription
517
+ }
518
+
519
+ /** Staged-envelope retention options. */
520
+ type Options = {
521
+ /** Clock used for staging timestamps. */
522
+ now?: (() => Date) | undefined
523
+ /** Staged-envelope TTL in ms; defaults to {@link deliveryRetentionMs}. */
524
+ ttl?: number | undefined
525
+ }
526
+
527
+ /** Created-row count plus references for every requested obligation. */
528
+ type Result = {
529
+ /** Newly staged obligations. */
530
+ created: number
531
+ /** Compact references for all inputs, including pre-existing rows. */
532
+ references: QueueReference[]
533
+ }
534
+ }
535
+
536
+ /** Claims one due pending obligation; misses classify why the claim is unavailable. */
537
+ export function claimQueueEvent(
538
+ db: Db.Db,
539
+ reference: QueueReference,
540
+ ): Promise<WebhookQueueEvents.claim.Result> {
541
+ const now = new Date()
542
+ return WebhookQueueEvents.claim(db, reference, {
543
+ now: now.toISOString(),
544
+ staleBefore: new Date(now.getTime() - deliveryAttemptStaleMs).toISOString(),
545
+ })
546
+ }
547
+
548
+ /**
549
+ * Marks a claimed obligation terminal and restarts its retention window, so
550
+ * the dedupe row outlives replays. Fenced by the claim marker: a stale
551
+ * claimant's completion returns false instead of clobbering a newer claim.
552
+ */
553
+ export function completeQueueEvent(
554
+ db: Db.Db,
555
+ reference: QueueReference,
556
+ options: completeQueueEvent.Options,
557
+ ): Promise<boolean> {
558
+ return WebhookQueueEvents.complete(db, reference, {
559
+ ...options,
560
+ expiresAt: new Date(Date.now() + deliveryRetentionMs).toISOString(),
561
+ })
562
+ }
563
+
564
+ export declare namespace completeQueueEvent {
565
+ /** Fenced terminal transition. */
566
+ type Options = {
567
+ /** Claim marker from the claimed record's `attemptingAt`. */
568
+ claimedAt: string
569
+ /** Terminal obligation outcome. */
570
+ status: WebhookQueueEvents.TerminalStatus
571
+ }
572
+ }
573
+
574
+ /**
575
+ * Releases a claim and schedules the next attempt on the retry backoff.
576
+ * Fenced by the claim marker like {@link completeQueueEvent}.
577
+ */
578
+ export function scheduleQueueEventRetry(
579
+ db: Db.Db,
580
+ reference: QueueReference,
581
+ options: scheduleQueueEventRetry.Options,
582
+ ): Promise<boolean> {
583
+ return WebhookQueueEvents.schedule(db, reference, {
584
+ claimedAt: options.claimedAt,
585
+ nextAttemptAt: new Date(Date.now() + retryDelayMs(options.attemptCount)).toISOString(),
586
+ })
587
+ }
588
+
589
+ export declare namespace scheduleQueueEventRetry {
590
+ /** Fenced retry scheduling. */
591
+ type Options = {
592
+ /** Attempts claimed so far; drives the backoff position. */
593
+ attemptCount: number
594
+ /** Claim marker from the claimed record's `attemptingAt`. */
595
+ claimedAt: string
596
+ }
597
+ }
598
+
599
+ /** Lists due pending references for the sweeper, oldest first. */
600
+ export function dueQueueEvents(db: Db.Db, limit: number): Promise<QueueReference[]> {
601
+ const now = new Date()
602
+ return WebhookQueueEvents.due(db, {
603
+ limit,
604
+ now: now.toISOString(),
605
+ staleBefore: new Date(now.getTime() - deliveryAttemptStaleMs).toISOString(),
606
+ })
607
+ }
608
+
609
+ /** Returns the authoritative pending-obligation snapshot. */
610
+ export function pendingQueueEvents(db: Db.Db): Promise<WebhookQueueEvents.pending.Result> {
611
+ const now = new Date()
612
+ return WebhookQueueEvents.pending(db, {
613
+ now: now.toISOString(),
614
+ staleBefore: new Date(now.getTime() - deliveryAttemptStaleMs).toISOString(),
615
+ })
616
+ }
617
+
618
+ /** Reads a full staged envelope for a compact Queue reference. */
619
+ export async function getQueueEvent(
620
+ db: Db.Db,
621
+ reference: QueueReference,
622
+ ): Promise<QueueEvent | null> {
623
+ return WebhookQueueEvents.get(db, reference)
624
+ }
625
+
626
+ /** Loads a staged envelope and delivers it with the subscription's current configuration. */
627
+ export async function deliverQueueEventAndRecord(
628
+ db: Db.Db,
629
+ reference: QueueReference,
630
+ options: deliverAndRecord.Options = {},
631
+ ): Promise<deliverQueueEventAndRecord.Result> {
632
+ const dequeuedAt = Date.now()
633
+ const event = await getQueueEvent(db, reference)
634
+ if (!event) return { status: 'missing' }
635
+ return deliverClaimedAndRecord(db, event, { ...options, dequeuedAt })
636
+ }
637
+
638
+ /**
639
+ * Delivers an already-loaded obligation envelope with the subscription's
640
+ * current configuration, skipping the staged-row read a claim already paid.
641
+ */
642
+ export async function deliverClaimedAndRecord(
643
+ db: Db.Db,
644
+ claimed: deliverClaimedAndRecord.Claimed,
645
+ options: deliverClaimedAndRecord.Options = {},
646
+ ): Promise<deliverClaimedAndRecord.Result> {
647
+ const dequeuedAt = options.dequeuedAt ?? Date.now()
648
+ const subscriptionReadStartedAt = Date.now()
649
+ const subscription = await WebhookSubscriptions.getById(
650
+ db,
651
+ claimed.subscriptionId,
652
+ new Date().toISOString(),
653
+ )
654
+ if (!subscription || subscription.status !== 'active') return { status: 'skipped' }
655
+ return {
656
+ result: await deliverAndRecord(db, subscription, claimed.envelope, {
657
+ ...options,
658
+ dequeuedAt,
659
+ subscriptionReadMs: Date.now() - subscriptionReadStartedAt,
660
+ }),
661
+ status: 'delivered',
662
+ }
663
+ }
664
+
665
+ export declare namespace deliverClaimedAndRecord {
666
+ /** Envelope and owning subscription id from a claimed obligation. */
667
+ type Claimed = {
668
+ /** Immutable event envelope to deliver. */
669
+ envelope: Envelope
670
+ /** Subscription re-resolved for current status and secret. */
671
+ subscriptionId: string
672
+ }
673
+
674
+ /** Delivery options plus the dequeue timestamp of the claiming path. */
675
+ type Options = deliverAndRecord.Options & {
676
+ /** Epoch ms the claiming message was dequeued; defaults to now. */
677
+ dequeuedAt?: number | undefined
678
+ }
679
+
680
+ /** A delivered event or an inactive subscription. */
681
+ type Result =
682
+ | {
683
+ /** Transport result from the current subscription state. */
684
+ result: WebhookDestination.Result
685
+ /** The envelope was delivered. */
686
+ status: 'delivered'
687
+ }
688
+ | {
689
+ /** The current subscription is inactive or deleted. */
690
+ status: 'skipped'
691
+ }
692
+ }
693
+
694
+ export declare namespace deliverQueueEventAndRecord {
695
+ /** A delivered event, inactive subscription, or stale duplicate reference. */
696
+ type Result =
697
+ | {
698
+ /** Transport result from the current subscription state. */
699
+ result: WebhookDestination.Result
700
+ /** The staged event was delivered. */
701
+ status: 'delivered'
702
+ }
703
+ | {
704
+ /** The staged event or current subscription was not deliverable. */
705
+ status: 'missing' | 'skipped'
706
+ }
707
+ }
708
+
443
709
  /** Records one delivery-log row (stateful, owner-agnostic; scoped per subscription). */
444
710
  export async function recordDelivery(
445
711
  db: Db.Db,
@@ -587,13 +853,21 @@ export async function deliverAndRecord(
587
853
  ): Promise<WebhookDestination.Result> {
588
854
  const startedAt = Date.now()
589
855
  // Snapshot the attempt before recordSuccess/recordFailure mutate the counter.
590
- const attempt = subscription.failureCount + 1
591
- const result = await WebhookDestination.from(subscription.destination).deliver({
592
- envelope,
593
- secret: subscription.secret,
594
- ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
595
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
596
- })
856
+ const attempt = options.attempt ?? subscription.failureCount + 1
857
+ const result = await (async () => {
858
+ try {
859
+ return await WebhookDestination.from(subscription.destination).deliver({
860
+ envelope,
861
+ secret: subscription.secret,
862
+ ...(options.fetch === undefined ? {} : { fetch: options.fetch }),
863
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
864
+ })
865
+ } catch {
866
+ // A formatting failure is a failed attempt, not an exception that bypasses
867
+ // subscription failure state and delivery metrics on every queue retry.
868
+ return { error: 'delivery failed before request', ok: false }
869
+ }
870
+ })()
597
871
  const stateWriteStartedAt = Date.now()
598
872
  if (result.ok) await recordSuccess(db, subscription, options)
599
873
  else await recordFailure(db, subscription, options)
@@ -625,6 +899,7 @@ export async function deliverAndRecord(
625
899
  queueAttempt: options.queueAttempt,
626
900
  result,
627
901
  subscription,
902
+ attempt,
628
903
  timings: {
629
904
  deliveryLogMs: completedAt - deliveryLogStartedAt,
630
905
  envelopeToQueueMs:
@@ -667,6 +942,12 @@ function eventTimestamp(envelope: Envelope): number | undefined {
667
942
  export declare namespace deliverAndRecord {
668
943
  /** Options for {@link deliverAndRecord}. */
669
944
  type Options = {
945
+ /**
946
+ * Delivery-log attempt number; the ledger claim count on the queue path.
947
+ * Defaults to the subscription failure counter, which concurrent
948
+ * deliveries can read stale.
949
+ */
950
+ attempt?: number | undefined
670
951
  /** Delivery-log row TTL in ms; defaults to {@link deliveryRetentionMs}. */
671
952
  deliveryRetentionMs?: number | undefined
672
953
  /** Queue dequeue time as epoch milliseconds, captured before subscription lookup. */
@@ -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',
@@ -7,13 +7,15 @@ import * as Squid from './squid.js'
7
7
  import * as Symbiosis from './symbiosis.js'
8
8
 
9
9
  const now = () => new Date('2026-01-01T00:00:00.000Z')
10
+ const evmUserAddress = TestFunding.relayQuoteUsers['eip155:4217']
11
+ const solanaUserAddress = '7ZwiMrYFNCzZ4TcPkeMx7BTZSZ9jDgGR8H8Nkazhr6L1'
10
12
  const providers = [
11
13
  Across.across({
12
14
  apiKey: 'across-api-key',
13
15
  integratorId: '0xdead',
14
16
  userAddress: '0x1111111111111111111111111111111111111111',
15
17
  }),
16
- Relay.relay({ userAddress: '0x1111111111111111111111111111111111111111' }),
18
+ Relay.relay({ userAddresses: TestFunding.relayQuoteUsers }),
17
19
  Rhino.rhino(),
18
20
  Squid.squid({
19
21
  integratorId: 'squid-integrator-id',
@@ -141,7 +143,7 @@ describe('funding providers', () => {
141
143
  apiKey: 'relay-api-key',
142
144
  fetch,
143
145
  now,
144
- userAddress: '0x1111111111111111111111111111111111111111',
146
+ userAddresses: TestFunding.relayQuoteUsers,
145
147
  })
146
148
 
147
149
  const result = await provider.getQuote(getQuoteInput(candidate), new AbortController().signal)
@@ -192,6 +194,74 @@ describe('funding providers', () => {
192
194
  `)
193
195
  })
194
196
 
197
+ test('requires a Relay quote wallet for the source chain', async () => {
198
+ const requests: RecordedRequest[] = []
199
+ const candidate = quoteCandidate('relay', {
200
+ destinationToken: 'pathUSD',
201
+ sourceChain: 'solana',
202
+ })
203
+ const provider = Relay.relay({
204
+ fetch: recordingFetch(requests, async () => jsonResponse({})),
205
+ now,
206
+ userAddresses: { 'eip155:4217': evmUserAddress },
207
+ })
208
+
209
+ const result = await provider.getQuote(getQuoteInput(candidate), new AbortController().signal)
210
+
211
+ expect(requests).toHaveLength(0)
212
+ expect(result).toMatchObject({
213
+ quality: {
214
+ sourceDetail: 'relay:quote-v2:QUOTE_USER_NOT_CONFIGURED',
215
+ tier: 'unavailable',
216
+ },
217
+ status: 'unavailable',
218
+ })
219
+ })
220
+
221
+ test('uses chain-specific Relay quote wallets', async () => {
222
+ const requests: RecordedRequest[] = []
223
+ const candidate = quoteCandidate('relay', {
224
+ destinationToken: 'pathUSD',
225
+ sourceChain: 'solana',
226
+ })
227
+ const fetch = recordingFetch(requests, async () =>
228
+ jsonResponse({
229
+ details: {
230
+ currencyOut: { amount: '990000', minimumAmount: '980000' },
231
+ timeEstimate: 15,
232
+ },
233
+ }),
234
+ )
235
+ const provider = Relay.relay({
236
+ fetch,
237
+ now,
238
+ userAddresses: {
239
+ 'eip155:4217': evmUserAddress,
240
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': solanaUserAddress,
241
+ },
242
+ })
243
+
244
+ const result = await provider.getQuote(getQuoteInput(candidate), new AbortController().signal)
245
+
246
+ expect(jsonRequestBody(requests[0])).toMatchObject({
247
+ destinationChainId: 4217,
248
+ originChainId: 792703809,
249
+ recipient: evmUserAddress,
250
+ user: solanaUserAddress,
251
+ })
252
+ expect(result).toMatchObject({ status: 'available' })
253
+ })
254
+
255
+ test('rejects invalid chain-specific Relay quote wallets', () => {
256
+ expect(() =>
257
+ Relay.relay({
258
+ userAddresses: {
259
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': '0x1111111111111111111111111111111111111111',
260
+ },
261
+ }),
262
+ ).toThrowErrorMatchingInlineSnapshot(`[FundingProvider.ProviderConfigurationError]`)
263
+ })
264
+
195
265
  test('maps successful Squid quotes to available quote results', async () => {
196
266
  const requests: RecordedRequest[] = []
197
267
  const candidate = quoteCandidate('squid')
@@ -646,7 +716,7 @@ describe('funding providers', () => {
646
716
  try {
647
717
  const provider = Relay.relay({
648
718
  now,
649
- userAddress: '0x1111111111111111111111111111111111111111',
719
+ userAddresses: TestFunding.relayQuoteUsers,
650
720
  })
651
721
  const result = await provider.getQuote(getQuoteInput(candidate), new AbortController().signal)
652
722
 
@@ -123,7 +123,6 @@ describe.skipIf(!apiKey)('relay.prepareTransfer (live)', () => {
123
123
  requestBody = JSON.parse(init.body) as Record<string, unknown>
124
124
  return globalThis.fetch(input, init)
125
125
  },
126
- userAddress: sender,
127
126
  })
128
127
  result = await provider.prepareTransfer!(prepareInput(provider), new AbortController().signal)
129
128
  })
@@ -172,7 +171,6 @@ describe.skipIf(!apiKey)('relay.createDepositAddress (live)', () => {
172
171
  return globalThis.fetch(input, init)
173
172
  },
174
173
  subsidies: { maxAmount: '100' },
175
- userAddress: sender,
176
174
  })
177
175
  result = await provider.createDepositAddress!(
178
176
  { ...depositAddressInput(provider), subsidize: true },
@@ -410,9 +408,7 @@ describe('relay.createDepositAddress', () => {
410
408
 
411
409
  describe('relay.prepareTransfer', () => {
412
410
  test('refuses methods and modes it cannot prepare', async () => {
413
- const provider = Relay.relay({
414
- userAddress: sender,
415
- })
411
+ const provider = Relay.relay()
416
412
  await expect(
417
413
  provider.prepareTransfer!(
418
414
  { ...prepareInput(provider), method: 'depositAddress' },
@@ -1,4 +1,4 @@
1
- import { AbiEvent, AbiFunction, Address, Hash, Hex, RpcTransport } from 'ox'
1
+ import { AbiEvent, AbiFunction, Address, Base58, Bytes, Hash, Hex, RpcTransport } from 'ox'
2
2
  import { createClient, custom, fallback } from 'viem'
3
3
  import { getBlockNumber, getLogs, getTransaction, getTransactionReceipt } from 'viem/actions'
4
4
  import * as z from 'zod/mini'
@@ -253,11 +253,9 @@ export function parseDepositAddressRequests(value: unknown) {
253
253
 
254
254
  /** Creates a Relay funding provider backed by `/quote/v2`. */
255
255
  export function relay(options: relay.Options = {}): FundingProvider.Provider<'relay'> {
256
+ const userAddresses = parseUserAddresses(options.userAddresses)
256
257
  if (
257
- (options.userAddress !== undefined &&
258
- !Address.validate(options.userAddress, { strict: false })) ||
259
- (options.recipientAddress !== undefined &&
260
- !Address.validate(options.recipientAddress, { strict: false }))
258
+ Object.entries(userAddresses).some(([chainId, address]) => invalidUserAddress(chainId, address))
261
259
  )
262
260
  throw new FundingProvider.ProviderConfigurationError()
263
261
 
@@ -411,13 +409,22 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
411
409
  }
412
410
  },
413
411
  async getQuote({ candidate, route }, signal) {
414
- if (options.userAddress === undefined)
412
+ const userAddress = userAddresses[route.source.chain.id]
413
+ if (userAddress === undefined)
415
414
  return FundingProvider.unavailableResult({
416
415
  detail: 'relay:quote-v2',
417
416
  message: 'QUOTE_USER_NOT_CONFIGURED',
418
417
  now: now(),
419
418
  source: 'providerQuote',
420
419
  })
420
+ const recipientAddress = userAddresses[route.destination.chain.id]
421
+ if (recipientAddress === undefined)
422
+ return FundingProvider.unavailableResult({
423
+ detail: 'relay:quote-v2',
424
+ message: 'QUOTE_RECIPIENT_NOT_CONFIGURED',
425
+ now: now(),
426
+ source: 'providerQuote',
427
+ })
421
428
  const destinationChainId = relayChainId(route.destination.chain)
422
429
  const sourceChainId = relayChainId(route.source.chain)
423
430
  try {
@@ -428,9 +435,9 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
428
435
  destinationCurrency: candidate.destinationToken.address,
429
436
  originChainId: sourceChainId,
430
437
  originCurrency: candidate.sourceToken.address,
431
- recipient: options.recipientAddress ?? options.userAddress,
438
+ recipient: recipientAddress,
432
439
  tradeType: 'EXACT_INPUT',
433
- user: options.userAddress,
440
+ user: userAddress,
434
441
  },
435
442
  fetch,
436
443
  headers: options.apiKey ? { 'x-api-key': options.apiKey } : undefined,
@@ -621,12 +628,10 @@ export declare namespace relay {
621
628
  now?: (() => Date) | undefined
622
629
  /** EVM chain evidence used to accelerate authenticated provider delivery. */
623
630
  observation?: Observation | undefined
624
- /** Optional recipient override; defaults to `userAddress`. */
625
- recipientAddress?: string | undefined
626
631
  /** Deposit subsidy policy enabled for this integration. */
627
632
  subsidies?: Subsidy.Policy | undefined
628
- /** Optional wallet address for indicative quotes. */
629
- userAddress?: string | undefined
633
+ /** Optional source and destination quote wallets keyed by CAIP-2 chain id. */
634
+ userAddresses?: Readonly<Partial<Record<FundingChain.Id, string>>> | string | undefined
630
635
  /** Tempo infrastructure used to receive authenticated Relay webhooks. */
631
636
  webhook?: Webhook | undefined
632
637
  }
@@ -662,6 +667,45 @@ export declare namespace relay {
662
667
  | 'unknown_address'
663
668
  }
664
669
 
670
+ function parseUserAddresses(value: relay.Options['userAddresses']) {
671
+ if (!value) return {}
672
+ const addresses =
673
+ typeof value === 'string'
674
+ ? (() => {
675
+ try {
676
+ return JSON.parse(value) as unknown
677
+ } catch {
678
+ throw new FundingProvider.ProviderConfigurationError()
679
+ }
680
+ })()
681
+ : value
682
+ if (!addresses || typeof addresses !== 'object' || Array.isArray(addresses))
683
+ throw new FundingProvider.ProviderConfigurationError()
684
+ return addresses as Readonly<Partial<Record<FundingChain.Id, string>>>
685
+ }
686
+
687
+ function invalidUserAddress(chainId: string, address: unknown) {
688
+ if (typeof address !== 'string') return true
689
+ if (/^eip155:\d+$/.test(chainId)) return !Address.validate(address, { strict: false })
690
+ if (/^solana:[1-9A-HJ-NP-Za-km-z]+$/.test(chainId)) {
691
+ try {
692
+ return Base58.toBytes(address).length !== 32
693
+ } catch {
694
+ return true
695
+ }
696
+ }
697
+ if (!/^tron:0x[0-9a-fA-F]+$/.test(chainId)) return true
698
+ try {
699
+ const bytes = Base58.toBytes(address)
700
+ if (bytes.length !== 25 || bytes[0] !== 0x41) return true
701
+ const payload = bytes.slice(0, 21)
702
+ const checksum = Hash.sha256(Hash.sha256(payload), { as: 'Bytes' }).slice(0, 4)
703
+ return !Bytes.isEqual(bytes.slice(21), checksum)
704
+ } catch {
705
+ return true
706
+ }
707
+ }
708
+
665
709
  type DepositAddressObserverOptions = relay.Observation & {
666
710
  apiKey?: string | undefined
667
711
  baseUrl?: string | undefined
@@ -5,6 +5,7 @@ test('exports internal modules', () => {
5
5
  [
6
6
  "Auth",
7
7
  "Cache",
8
+ "Credential",
8
9
  "Cursor",
9
10
  "EdgeCache",
10
11
  "EarnVaults",
@@ -1,5 +1,6 @@
1
1
  export * as Auth from './Auth.js'
2
2
  export * as Cache from './Cache.js'
3
+ export * as Credential from './Credential.js'
3
4
  export * as Cursor from './Cursor.js'
4
5
  export * as EdgeCache from './EdgeCache.js'
5
6
  export * as EarnVaults from './EarnVaults.js'