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
@@ -21,6 +21,8 @@ const followUpDelaySeconds = { delivery: 1, provider: 5, settlement: 2 } as cons
21
21
  >
22
22
  const maximumPages = 100
23
23
  const maximumFollowUpAttempts = 6
24
+ const providerFailureStatuses = ['failure'] as const
25
+ const providerRefundStatuses = ['refund', 'refunded'] as const
24
26
 
25
27
  /** Verified token transfer returned by an authoritative chain reader. */
26
28
  export type TransferEvidence = {
@@ -866,6 +868,16 @@ export async function reconcileRequest(
866
868
  }
867
869
  if (verified.length === 0) return { created, terminal: false, updated: updatedIds.size }
868
870
  if (!sourceEvidenceComplete) return { created, terminal: false, updated: updatedIds.size }
871
+ const recovery = await reconcileProviderRecovery(db, { ...options, verified })
872
+ if (recovery) {
873
+ for (const id of recovery.updatedIds) updatedIds.add(id)
874
+ return {
875
+ created,
876
+ ...(recovery.followUp ? { followUp: recovery.followUp } : {}),
877
+ terminal: recovery.terminal,
878
+ updated: updatedIds.size,
879
+ }
880
+ }
869
881
  const delivery = await reconcileDelivery(db, { ...options, verified })
870
882
  for (const id of delivery.updatedIds) updatedIds.add(id)
871
883
  return {
@@ -910,6 +922,130 @@ export declare namespace reconcileRequest {
910
922
  }
911
923
  }
912
924
 
925
+ async function reconcileProviderRecovery(db: Db.Db, options: ReconcileDeliveryOptions) {
926
+ const failed = (providerFailureStatuses as readonly string[]).includes(
927
+ options.request?.status ?? '',
928
+ )
929
+ const refunded = (providerRefundStatuses as readonly string[]).includes(
930
+ options.request?.status ?? '',
931
+ )
932
+ if (!refunded && !failed) return undefined
933
+ if (failed) {
934
+ const destinationEvidence = await Promise.all(
935
+ unique(options.request?.destinationTransactionHashes ?? []).map((transactionHash) =>
936
+ options.verifyTransfers({
937
+ chain: options.address.snapshot.destinationChain,
938
+ recipient: options.address.recipient,
939
+ token: options.address.providerOutputToken,
940
+ transactionHash,
941
+ }),
942
+ ),
943
+ )
944
+ if (destinationEvidence.some((transfers) => transfers.length > 0)) return undefined
945
+ }
946
+
947
+ const updatedIds = new Set<string>()
948
+ if (options.verified.every((entry) => Deposit.isTerminal(entry.deposit.status)))
949
+ return { terminal: true, updatedIds }
950
+
951
+ // Relay auto-refunds may include an internal settlement transaction; only matching source-token transfers become public evidence.
952
+ const providerRefundTransactionHashes = unique(options.request?.refundTransactionHashes ?? [])
953
+ const refundEvidence = await Promise.all(
954
+ providerRefundTransactionHashes.map((transactionHash) =>
955
+ options.verifyTransfers({
956
+ chain: options.address.snapshot.sourceChain,
957
+ recipient: options.address.refundAddress,
958
+ token: options.address.snapshot.sourceToken,
959
+ transactionHash,
960
+ }),
961
+ ),
962
+ )
963
+ const refundTransfers = refundEvidence.flat()
964
+ const refundOutput = refundTransfers.reduce(
965
+ (total, transfer) => total + BigInt(transfer.amount),
966
+ 0n,
967
+ )
968
+ const refundTransactionHashes = unique(
969
+ refundTransfers.map((transfer) => transfer.transactionHash),
970
+ )
971
+ // Relay can include internal transactions, so its reported amount proves when all matching refund transfers are visible.
972
+ const expectedRefundOutput = options.request?.refundAmountExpected
973
+ ? BigInt(options.request.refundAmountExpected)
974
+ : undefined
975
+ const refundEvidenceComplete = (() => {
976
+ if (refundOutput === 0n) return false
977
+ if (expectedRefundOutput !== undefined) return refundOutput === expectedRefundOutput
978
+ return refundEvidence.length > 0 && refundEvidence.every((transfers) => transfers.length > 0)
979
+ })()
980
+ const totalSource = options.verified.reduce((total, entry) => total + entry.sourceAmount, 0n)
981
+ let allocated = 0n
982
+ let cumulativeSource = 0n
983
+ let terminal = refundEvidenceComplete
984
+
985
+ // Include terminal rows in proportional allocation so retries preserve the original split without reallocating their verified refunds.
986
+ for (const entry of options.verified) {
987
+ cumulativeSource += entry.sourceAmount
988
+ const nextAllocated = totalSource === 0n ? 0n : (refundOutput * cumulativeSource) / totalSource
989
+ const amount = nextAllocated - allocated
990
+ allocated = nextAllocated
991
+ if (Deposit.isTerminal(entry.deposit.status)) continue
992
+ let deposit = entry.deposit
993
+ if (deposit.status !== 'refunding') {
994
+ const result = await Deposit.transition(db, {
995
+ expectedVersion: deposit.version,
996
+ id: deposit.id,
997
+ providerTransactionHashes: unique([
998
+ ...deposit.providerTransactionHashes,
999
+ ...providerRefundTransactionHashes,
1000
+ ]),
1001
+ status: 'refunding',
1002
+ })
1003
+ if (result.type === 'applied') {
1004
+ deposit = result.record
1005
+ updatedIds.add(deposit.id)
1006
+ } else if (result.type === 'stale') deposit = result.current
1007
+ }
1008
+ if (!refundEvidenceComplete) continue
1009
+ if (Deposit.isTerminal(deposit.status)) continue
1010
+ if (deposit.status !== 'refunding') {
1011
+ terminal = false
1012
+ continue
1013
+ }
1014
+ const result = await Deposit.transition(db, {
1015
+ expectedVersion: deposit.version,
1016
+ id: deposit.id,
1017
+ snapshot: {
1018
+ ...deposit.snapshot,
1019
+ refundAmount: Value.tokenAmount({
1020
+ baseUnits: amount,
1021
+ currency: options.address.snapshot.sourceToken.currency,
1022
+ decimals: options.address.snapshot.sourceToken.decimals,
1023
+ }),
1024
+ refundTransactionHashes,
1025
+ },
1026
+ status: 'refunded',
1027
+ })
1028
+ if (result.type === 'applied') updatedIds.add(deposit.id)
1029
+ else if (result.type !== 'stale' || !Deposit.isTerminal(result.current.status)) terminal = false
1030
+ }
1031
+
1032
+ const outcome = (() => {
1033
+ if (refundEvidenceComplete) return 'verified'
1034
+ // Relay failure can be followed by an automatic refund; durable exhaustion owns escalation to action-required.
1035
+ if (failed) return 'provider_failed'
1036
+ return 'pending'
1037
+ })()
1038
+ options.metrics?.count('funding_deposit_refund_count', 1, {
1039
+ outcome,
1040
+ provider: options.address.providerId,
1041
+ })
1042
+ return {
1043
+ ...(!refundEvidenceComplete ? { followUp: 'delivery' as const } : {}),
1044
+ terminal,
1045
+ updatedIds,
1046
+ }
1047
+ }
1048
+
913
1049
  async function reconcileDelivery(db: Db.Db, options: ReconcileDeliveryOptions) {
914
1050
  let followUp: reconcileRequest.ReturnType['followUp']
915
1051
  let terminal = true
@@ -188,6 +188,7 @@ describe('observe', () => {
188
188
  createdAt: '2026-01-01T00:00:02.000Z',
189
189
  destinationTransactionHashes: [],
190
190
  id: 'relay-source-request',
191
+ refundTransactionHashes: [],
191
192
  sourceTransactionHashes: [sourceTransactionHash],
192
193
  status: 'pending',
193
194
  updatedAt: '2026-01-01T00:00:03.000Z',
@@ -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
  })
@@ -151,11 +150,8 @@ describe.skipIf(!apiKey)('relay.prepareTransfer (live)', () => {
151
150
  expect(result.correlation.requestId).toMatch(/^0x[0-9a-f]{64}$/)
152
151
  expect(result.correlation.checkEndpoint).toContain('/intents/status/v3')
153
152
  expect(BigInt(result.destinationAmount) > 0n).toBe(true)
153
+ expect(BigInt(result.destinationAmountMin) > 0n).toBe(true)
154
154
  expect(BigInt(result.destinationAmountMin) <= BigInt(result.destinationAmount)).toBe(true)
155
- // The forwarded slippage bound holds on the quoted minimum.
156
- expect(
157
- BigInt(result.destinationAmountMin) >= (BigInt(result.destinationAmount) * 9_700n) / 10_000n,
158
- ).toBe(true)
159
155
  // Relay returns no quote validity window; the fan-out applies the default.
160
156
  expect(result.expiresAt).toBeUndefined()
161
157
  expect(Number.isNaN(Date.parse(result.sampledAt))).toBe(false)
@@ -175,7 +171,6 @@ describe.skipIf(!apiKey)('relay.createDepositAddress (live)', () => {
175
171
  return globalThis.fetch(input, init)
176
172
  },
177
173
  subsidies: { maxAmount: '100' },
178
- userAddress: sender,
179
174
  })
180
175
  result = await provider.createDepositAddress!(
181
176
  { ...depositAddressInput(provider), subsidize: true },
@@ -413,9 +408,7 @@ describe('relay.createDepositAddress', () => {
413
408
 
414
409
  describe('relay.prepareTransfer', () => {
415
410
  test('refuses methods and modes it cannot prepare', async () => {
416
- const provider = Relay.relay({
417
- userAddress: sender,
418
- })
411
+ const provider = Relay.relay()
419
412
  await expect(
420
413
  provider.prepareTransfer!(
421
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'
@@ -17,6 +17,7 @@ const defaultBaseUrl = 'https://api.relay.link'
17
17
  const defaultFetch: FundingProvider.Fetch = (input, init) => globalThis.fetch(input, init)
18
18
  const deliveryBackfillBlocks = 256n
19
19
  const relayChainsCacheTtlMs = 60_000
20
+ const relayRefundStatuses = ['refund', 'refunded'] as const
20
21
  const sourceSweepForwardBlocks = 256n
21
22
  const depositErc20FullAllowance = AbiFunction.from(
22
23
  'function depositErc20(address depositor,address token,bytes32 id)',
@@ -46,6 +47,7 @@ const webhookStatuses = [
46
47
  'fallback',
47
48
  'pending',
48
49
  'refund',
50
+ 'refunded',
49
51
  'submitted',
50
52
  'success',
51
53
  'waiting',
@@ -196,6 +198,13 @@ namespace schema {
196
198
  ),
197
199
  ),
198
200
  id: z.string().check(z.minLength(1)),
201
+ refundCurrencyData: z.optional(
202
+ z.nullable(
203
+ z.object({
204
+ amount: z.string().check(z.regex(/^\d+$/)),
205
+ }),
206
+ ),
207
+ ),
199
208
  protocol: z.optional(
200
209
  z.nullable(
201
210
  z.object({
@@ -244,11 +253,9 @@ export function parseDepositAddressRequests(value: unknown) {
244
253
 
245
254
  /** Creates a Relay funding provider backed by `/quote/v2`. */
246
255
  export function relay(options: relay.Options = {}): FundingProvider.Provider<'relay'> {
256
+ const userAddresses = parseUserAddresses(options.userAddresses)
247
257
  if (
248
- (options.userAddress !== undefined &&
249
- !Address.validate(options.userAddress, { strict: false })) ||
250
- (options.recipientAddress !== undefined &&
251
- !Address.validate(options.recipientAddress, { strict: false }))
258
+ Object.entries(userAddresses).some(([chainId, address]) => invalidUserAddress(chainId, address))
252
259
  )
253
260
  throw new FundingProvider.ProviderConfigurationError()
254
261
 
@@ -402,13 +409,22 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
402
409
  }
403
410
  },
404
411
  async getQuote({ candidate, route }, signal) {
405
- if (options.userAddress === undefined)
412
+ const userAddress = userAddresses[route.source.chain.id]
413
+ if (userAddress === undefined)
406
414
  return FundingProvider.unavailableResult({
407
415
  detail: 'relay:quote-v2',
408
416
  message: 'QUOTE_USER_NOT_CONFIGURED',
409
417
  now: now(),
410
418
  source: 'providerQuote',
411
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
+ })
412
428
  const destinationChainId = relayChainId(route.destination.chain)
413
429
  const sourceChainId = relayChainId(route.source.chain)
414
430
  try {
@@ -419,9 +435,9 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
419
435
  destinationCurrency: candidate.destinationToken.address,
420
436
  originChainId: sourceChainId,
421
437
  originCurrency: candidate.sourceToken.address,
422
- recipient: options.recipientAddress ?? options.userAddress,
438
+ recipient: recipientAddress,
423
439
  tradeType: 'EXACT_INPUT',
424
- user: options.userAddress,
440
+ user: userAddress,
425
441
  },
426
442
  fetch,
427
443
  headers: options.apiKey ? { 'x-api-key': options.apiKey } : undefined,
@@ -481,6 +497,8 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
481
497
  return {
482
498
  ...(parsed.continuation ? { continuation: parsed.continuation } : {}),
483
499
  requests: parsed.requests.map((request) => {
500
+ const outputTransactionHashes = request.data?.outTxs?.map((tx) => tx.txHash) ?? []
501
+ const refunded = (relayRefundStatuses as readonly string[]).includes(request.status)
484
502
  const correlation = (() => {
485
503
  const { protocol } = request
486
504
  if (!protocol) return undefined
@@ -499,9 +517,13 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
499
517
  })()
500
518
  return {
501
519
  createdAt: new Date(request.createdAt).toISOString(),
502
- destinationTransactionHashes: request.data?.outTxs?.map((tx) => tx.txHash) ?? [],
520
+ destinationTransactionHashes: refunded ? [] : outputTransactionHashes,
503
521
  id: request.id,
504
522
  ...(correlation ? { providerState: correlation } : {}),
523
+ ...(refunded && request.refundCurrencyData
524
+ ? { refundAmountExpected: request.refundCurrencyData.amount }
525
+ : {}),
526
+ refundTransactionHashes: refunded ? outputTransactionHashes : [],
505
527
  sourceTransactionHashes: request.depositAddress?.depositTxHash
506
528
  ? [request.depositAddress.depositTxHash]
507
529
  : [],
@@ -606,12 +628,10 @@ export declare namespace relay {
606
628
  now?: (() => Date) | undefined
607
629
  /** EVM chain evidence used to accelerate authenticated provider delivery. */
608
630
  observation?: Observation | undefined
609
- /** Optional recipient override; defaults to `userAddress`. */
610
- recipientAddress?: string | undefined
611
631
  /** Deposit subsidy policy enabled for this integration. */
612
632
  subsidies?: Subsidy.Policy | undefined
613
- /** Optional wallet address for indicative quotes. */
614
- 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
615
635
  /** Tempo infrastructure used to receive authenticated Relay webhooks. */
616
636
  webhook?: Webhook | undefined
617
637
  }
@@ -647,6 +667,45 @@ export declare namespace relay {
647
667
  | 'unknown_address'
648
668
  }
649
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
+
650
709
  type DepositAddressObserverOptions = relay.Observation & {
651
710
  apiKey?: string | undefined
652
711
  baseUrl?: string | undefined