tapimo 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/README.md +10 -7
  2. package/dist/ApiKey.d.ts +3 -0
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +10 -0
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +7 -3
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +9 -2
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +20117 -19183
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +61 -4
  13. package/dist/App.js.map +1 -1
  14. package/dist/admin/App.d.ts +84 -74
  15. package/dist/admin/App.d.ts.map +1 -1
  16. package/dist/admin/apps/api-keys.d.ts +14 -1
  17. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  18. package/dist/admin/apps/api-keys.js +6 -2
  19. package/dist/admin/apps/api-keys.js.map +1 -1
  20. package/dist/admin/apps/organizations.d.ts +2 -0
  21. package/dist/admin/apps/organizations.d.ts.map +1 -1
  22. package/dist/admin/apps/verified-tokens.d.ts +11 -11
  23. package/dist/admin/ui.generated.d.ts +1 -1
  24. package/dist/admin/ui.generated.js +67 -67
  25. package/dist/admin/ui.generated.js.map +1 -1
  26. package/dist/analytics/Analytics.d.ts +2 -0
  27. package/dist/analytics/Analytics.d.ts.map +1 -1
  28. package/dist/analytics/Analytics.js +3 -0
  29. package/dist/analytics/Analytics.js.map +1 -1
  30. package/dist/apps/data/App.d.ts +11696 -13937
  31. package/dist/apps/data/App.d.ts.map +1 -1
  32. package/dist/apps/data/App.js +4 -8
  33. package/dist/apps/data/App.js.map +1 -1
  34. package/dist/apps/data/routes/activities.d.ts +271 -271
  35. package/dist/apps/data/routes/addresses.d.ts +398 -167
  36. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  37. package/dist/apps/data/routes/balances.d.ts +278 -8
  38. package/dist/apps/data/routes/balances.d.ts.map +1 -1
  39. package/dist/apps/data/routes/balances.js +114 -2
  40. package/dist/apps/data/routes/balances.js.map +1 -1
  41. package/dist/apps/data/routes/blocks.d.ts +123 -123
  42. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  43. package/dist/apps/data/routes/earn.d.ts +498 -6
  44. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  45. package/dist/apps/data/routes/earn.js +876 -4
  46. package/dist/apps/data/routes/earn.js.map +1 -1
  47. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  48. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  49. package/dist/apps/data/routes/indexer.d.ts +6 -6
  50. package/dist/apps/data/routes/indexer.js +4 -1
  51. package/dist/apps/data/routes/indexer.js.map +1 -1
  52. package/dist/apps/data/routes/receipts.d.ts +235 -235
  53. package/dist/apps/data/routes/rpc.d.ts +31 -31
  54. package/dist/apps/data/routes/rpc.js +4 -1
  55. package/dist/apps/data/routes/rpc.js.map +1 -1
  56. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  57. package/dist/apps/data/routes/tokens.d.ts +449 -449
  58. package/dist/apps/data/routes/transactions.d.ts +2 -2
  59. package/dist/apps/data/routes/transfers.d.ts +81 -81
  60. package/dist/apps/data/routes/valuation.d.ts +1 -1
  61. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  62. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  63. package/dist/apps/data/routes/zones.d.ts +14 -14
  64. package/dist/apps/funding/Access.d.ts +16 -0
  65. package/dist/apps/funding/Access.d.ts.map +1 -0
  66. package/dist/apps/funding/Access.js +22 -0
  67. package/dist/apps/funding/Access.js.map +1 -0
  68. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1640 -3217
  69. package/dist/apps/funding/App.d.ts.map +1 -0
  70. package/dist/apps/funding/App.js +61 -0
  71. package/dist/apps/funding/App.js.map +1 -0
  72. package/dist/apps/funding/Inventory.d.ts +5 -0
  73. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  74. package/dist/apps/funding/Inventory.js +25 -0
  75. package/dist/apps/funding/Inventory.js.map +1 -0
  76. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  77. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  78. package/dist/apps/funding/ProviderAttempts.js +24 -0
  79. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  80. package/dist/apps/funding/index.d.ts +2 -0
  81. package/dist/apps/funding/index.d.ts.map +1 -0
  82. package/dist/apps/funding/index.js +2 -0
  83. package/dist/apps/funding/index.js.map +1 -0
  84. package/dist/apps/funding/routes/chains.d.ts +169 -0
  85. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/chains.js +80 -0
  87. package/dist/apps/funding/routes/chains.js.map +1 -0
  88. package/dist/apps/funding/routes/deposit-addresses.d.ts +1997 -0
  89. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/deposit-addresses.js +778 -0
  91. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  92. package/dist/apps/funding/routes/providers.d.ts +91 -0
  93. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/providers.js +56 -0
  95. package/dist/apps/funding/routes/providers.js.map +1 -0
  96. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  97. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/quotes.js +188 -0
  99. package/dist/apps/funding/routes/quotes.js.map +1 -0
  100. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  101. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  102. package/dist/apps/funding/routes/transfers.js +614 -0
  103. package/dist/apps/funding/routes/transfers.js.map +1 -0
  104. package/dist/apps/index.d.ts +1 -0
  105. package/dist/apps/index.d.ts.map +1 -1
  106. package/dist/apps/index.js +1 -0
  107. package/dist/apps/index.js.map +1 -1
  108. package/dist/apps/management/App.d.ts +430 -165
  109. package/dist/apps/management/App.d.ts.map +1 -1
  110. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  111. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  112. package/dist/apps/management/routes/api-keys.js +75 -1
  113. package/dist/apps/management/routes/api-keys.js.map +1 -1
  114. package/dist/apps/management/routes/billing.d.ts +6 -6
  115. package/dist/apps/management/routes/faucet.d.ts +35 -35
  116. package/dist/apps/management/routes/invitations.d.ts +122 -122
  117. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  118. package/dist/apps/management/routes/members.d.ts +69 -69
  119. package/dist/apps/management/routes/orgs.d.ts +99 -99
  120. package/dist/apps/management/routes/projects.d.ts +25 -25
  121. package/dist/apps/management/routes/scopes.d.ts +1 -1
  122. package/dist/apps/management/routes/usage.d.ts +72 -72
  123. package/dist/apps/management/routes/verified-token-requests.d.ts +77 -77
  124. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  125. package/dist/apps/mcp.d.ts +2 -3
  126. package/dist/apps/mcp.d.ts.map +1 -1
  127. package/dist/apps/mcp.js +16 -14
  128. package/dist/apps/mcp.js.map +1 -1
  129. package/dist/apps/metadata.d.ts +4 -0
  130. package/dist/apps/metadata.d.ts.map +1 -1
  131. package/dist/apps/metadata.js +10 -0
  132. package/dist/apps/metadata.js.map +1 -1
  133. package/dist/apps/mpp/App.d.ts +44 -44
  134. package/dist/cloudflare.d.ts +50 -0
  135. package/dist/cloudflare.d.ts.map +1 -1
  136. package/dist/cloudflare.js +178 -0
  137. package/dist/cloudflare.js.map +1 -1
  138. package/dist/db/Db.d.ts +2 -0
  139. package/dist/db/Db.d.ts.map +1 -1
  140. package/dist/db/Db.js +45 -0
  141. package/dist/db/Db.js.map +1 -1
  142. package/dist/db/Schema.d.ts +12 -0
  143. package/dist/db/Schema.d.ts.map +1 -1
  144. package/dist/db/Schema.js +20 -0
  145. package/dist/db/Schema.js.map +1 -1
  146. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  147. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  148. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  149. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  150. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  151. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  152. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  153. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  154. package/dist/db/tables/fundingDeposits.d.ts +74 -1
  155. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  156. package/dist/db/tables/fundingDeposits.js +126 -0
  157. package/dist/db/tables/fundingDeposits.js.map +1 -1
  158. package/dist/internal/Auth.d.ts +15 -11
  159. package/dist/internal/Auth.d.ts.map +1 -1
  160. package/dist/internal/Auth.js +52 -17
  161. package/dist/internal/Auth.js.map +1 -1
  162. package/dist/internal/Cache.js +1 -1
  163. package/dist/internal/Cache.js.map +1 -1
  164. package/dist/internal/EdgeCache.d.ts +2 -2
  165. package/dist/internal/EdgeCache.d.ts.map +1 -1
  166. package/dist/internal/EdgeCache.js +10 -4
  167. package/dist/internal/EdgeCache.js.map +1 -1
  168. package/dist/internal/IpAllowlist.d.ts +15 -0
  169. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  170. package/dist/internal/IpAllowlist.js +27 -0
  171. package/dist/internal/IpAllowlist.js.map +1 -0
  172. package/dist/internal/OpenApi.d.ts +1 -1
  173. package/dist/internal/OpenApi.js +1 -1
  174. package/dist/internal/OpenApi.js.map +1 -1
  175. package/dist/internal/Tidx.d.ts +6 -0
  176. package/dist/internal/Tidx.d.ts.map +1 -1
  177. package/dist/internal/Tidx.js +13 -3
  178. package/dist/internal/Tidx.js.map +1 -1
  179. package/dist/internal/Viem.d.ts +8 -0
  180. package/dist/internal/Viem.d.ts.map +1 -1
  181. package/dist/internal/Viem.js +12 -5
  182. package/dist/internal/Viem.js.map +1 -1
  183. package/dist/internal/funding/Deposit.d.ts +42 -0
  184. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  185. package/dist/internal/funding/Deposit.js +37 -5
  186. package/dist/internal/funding/Deposit.js.map +1 -1
  187. package/dist/internal/funding/Evidence.d.ts +1 -5
  188. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  189. package/dist/internal/funding/Evidence.js +4 -3
  190. package/dist/internal/funding/Evidence.js.map +1 -1
  191. package/dist/internal/funding/Provider.d.ts +102 -0
  192. package/dist/internal/funding/Provider.d.ts.map +1 -1
  193. package/dist/internal/funding/Provider.js +5 -0
  194. package/dist/internal/funding/Provider.js.map +1 -1
  195. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  196. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  197. package/dist/internal/funding/Reconciliation.js +348 -54
  198. package/dist/internal/funding/Reconciliation.js.map +1 -1
  199. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  200. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  201. package/dist/internal/funding/SourceObservation.js +143 -0
  202. package/dist/internal/funding/SourceObservation.js.map +1 -0
  203. package/dist/internal/funding/Subsidy.d.ts +2 -2
  204. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  205. package/dist/internal/funding/Subsidy.js +1 -1
  206. package/dist/internal/funding/Subsidy.js.map +1 -1
  207. package/dist/internal/funding/index.d.ts +1 -0
  208. package/dist/internal/funding/index.d.ts.map +1 -1
  209. package/dist/internal/funding/index.js +1 -0
  210. package/dist/internal/funding/index.js.map +1 -1
  211. package/dist/internal/funding/providers/relay.d.ts +43 -1
  212. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  213. package/dist/internal/funding/providers/relay.js +438 -27
  214. package/dist/internal/funding/providers/relay.js.map +1 -1
  215. package/package.json +3 -3
  216. package/src/ApiKey.test.ts +35 -0
  217. package/src/ApiKey.ts +27 -0
  218. package/src/ApiKeys.test.ts +29 -0
  219. package/src/ApiKeys.ts +11 -3
  220. package/src/App.test.ts +55 -5
  221. package/src/App.ts +79 -5
  222. package/src/Client.test-d.ts +203 -28
  223. package/src/admin/apps/api-keys.test.ts +25 -1
  224. package/src/admin/apps/api-keys.ts +6 -2
  225. package/src/admin/ui.generated.ts +67 -67
  226. package/src/analytics/Analytics.ts +4 -0
  227. package/src/apps/composition.test.ts +130 -3
  228. package/src/apps/data/App.test.ts +0 -12
  229. package/src/apps/data/App.ts +4 -8
  230. package/src/apps/data/routes/balances.test.ts +42 -0
  231. package/src/apps/data/routes/balances.ts +218 -85
  232. package/src/apps/data/routes/earn.test.ts +363 -0
  233. package/src/apps/data/routes/earn.ts +1141 -3
  234. package/src/apps/data/routes/indexer.ts +2 -1
  235. package/src/apps/data/routes/rpc.ts +2 -0
  236. package/src/apps/data/routes/tokens.test.ts +6 -8
  237. package/src/apps/funding/Access.ts +26 -0
  238. package/src/apps/funding/App.test.ts +78 -0
  239. package/src/apps/funding/App.ts +78 -0
  240. package/src/apps/funding/Inventory.ts +30 -0
  241. package/src/apps/funding/ProviderAttempts.ts +26 -0
  242. package/src/apps/funding/index.ts +1 -0
  243. package/src/apps/funding/routes/chains.test.ts +78 -0
  244. package/src/apps/funding/routes/chains.ts +101 -0
  245. package/src/apps/funding/routes/deposit-addresses.test.ts +1649 -0
  246. package/src/apps/funding/routes/deposit-addresses.ts +932 -0
  247. package/src/apps/funding/routes/providers.test.ts +165 -0
  248. package/src/apps/funding/routes/providers.ts +73 -0
  249. package/src/apps/funding/routes/quotes.test.ts +511 -0
  250. package/src/apps/funding/routes/quotes.ts +205 -0
  251. package/src/apps/funding/routes/transfers.test.ts +1236 -0
  252. package/src/apps/funding/routes/transfers.ts +704 -0
  253. package/src/apps/index.ts +1 -0
  254. package/src/apps/management/routes/api-keys.test.ts +98 -0
  255. package/src/apps/management/routes/api-keys.ts +96 -1
  256. package/src/apps/mcp.test.ts +17 -2
  257. package/src/apps/mcp.ts +18 -13
  258. package/src/apps/metadata.ts +12 -0
  259. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  260. package/src/cloudflare.ts +279 -0
  261. package/src/db/Db.ts +71 -0
  262. package/src/db/Schema.ts +24 -0
  263. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  264. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  265. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  266. package/src/db/tables/fundingDeposits.test.ts +239 -0
  267. package/src/db/tables/fundingDeposits.ts +214 -1
  268. package/src/internal/Auth.test.ts +205 -5
  269. package/src/internal/Auth.ts +63 -19
  270. package/src/internal/Cache.test.ts +37 -3
  271. package/src/internal/Cache.ts +1 -1
  272. package/src/internal/EdgeCache.test.ts +7 -7
  273. package/src/internal/EdgeCache.ts +13 -4
  274. package/src/internal/IpAllowlist.test.ts +52 -0
  275. package/src/internal/IpAllowlist.ts +39 -0
  276. package/src/internal/Log.test.ts +7 -1
  277. package/src/internal/OpenApi.ts +1 -1
  278. package/src/internal/Tidx.test.ts +28 -0
  279. package/src/internal/Tidx.ts +19 -3
  280. package/src/internal/Viem.test.ts +28 -0
  281. package/src/internal/Viem.ts +21 -5
  282. package/src/internal/funding/Deposit.ts +74 -5
  283. package/src/internal/funding/Evidence.test.ts +2 -2
  284. package/src/internal/funding/Evidence.ts +4 -9
  285. package/src/internal/funding/Provider.ts +115 -0
  286. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  287. package/src/internal/funding/Reconciliation.test.ts +1364 -109
  288. package/src/internal/funding/Reconciliation.ts +467 -88
  289. package/src/internal/funding/SourceObservation.test.ts +301 -0
  290. package/src/internal/funding/SourceObservation.ts +267 -0
  291. package/src/internal/funding/Subsidy.ts +2 -2
  292. package/src/internal/funding/index.test.ts +1 -0
  293. package/src/internal/funding/index.ts +1 -0
  294. package/src/internal/funding/providers/relay.test.ts +94 -4
  295. package/src/internal/funding/providers/relay.ts +533 -27
  296. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  297. package/dist/apps/data/routes/funding.js +0 -1557
  298. package/dist/apps/data/routes/funding.js.map +0 -1
  299. package/src/apps/data/routes/funding.test.ts +0 -2994
  300. package/src/apps/data/routes/funding.ts +0 -1821
@@ -1,15 +1,55 @@
1
- import { Address, Hash, Hex } from 'ox'
1
+ import { AbiEvent, AbiFunction, Address, Hash, Hex, RpcTransport } from 'ox'
2
+ import { createClient, custom, fallback } from 'viem'
3
+ import { getBlockNumber, getLogs, getTransaction, getTransactionReceipt } from 'viem/actions'
2
4
  import * as z from 'zod/mini'
3
5
 
4
6
  import * as Db from '../../../db/Db.js'
5
7
  import * as FundingDepositAddresses from '../../../db/tables/fundingDepositAddresses.js'
8
+ import * as FundingDepositRequestObservations from '../../../db/tables/fundingDepositRequestObservations.js'
9
+ import * as Viem from '../../Viem.js'
10
+ import * as FundingCatalog from '../Catalog.js'
6
11
  import * as FundingChain from '../Chain.js'
12
+ import * as Evidence from '../Evidence.js'
7
13
  import * as FundingProvider from '../Provider.js'
8
14
  import * as Subsidy from '../Subsidy.js'
9
15
 
10
16
  const defaultBaseUrl = 'https://api.relay.link'
11
17
  const defaultFetch: FundingProvider.Fetch = (input, init) => globalThis.fetch(input, init)
12
- const webhookSignatureToleranceSeconds = 300
18
+ const deliveryBackfillBlocks = 256n
19
+ const relayChainsCacheTtlMs = 60_000
20
+ const sourceSweepForwardBlocks = 256n
21
+ const depositErc20FullAllowance = AbiFunction.from(
22
+ 'function depositErc20(address depositor,address token,bytes32 id)',
23
+ )
24
+ const depositErc20WithAmount = AbiFunction.from(
25
+ 'function depositErc20(address depositor,address token,uint256 amount,bytes32 id)',
26
+ )
27
+ const relayErc20DepositEvent = AbiEvent.from(
28
+ 'event RelayErc20Deposit(address from, address token, uint256 amount, bytes32 id)',
29
+ )
30
+ const relayTransfer = AbiFunction.from('function transfer(address recipient,uint256 amount)')
31
+ const transferEvent = AbiEvent.from(
32
+ 'event Transfer(address indexed from, address indexed to, uint256 value)',
33
+ )
34
+ const webhookSignatureToleranceMs = 300_000
35
+ const deliveryObservationStatuses = [
36
+ 'depositing',
37
+ 'pending',
38
+ 'submitted',
39
+ 'success',
40
+ 'waiting',
41
+ ] as const
42
+ // Bound provider statuses before using them as metric tags.
43
+ const webhookStatuses = [
44
+ 'depositing',
45
+ 'failure',
46
+ 'fallback',
47
+ 'pending',
48
+ 'refund',
49
+ 'submitted',
50
+ 'success',
51
+ 'waiting',
52
+ ] as const
13
53
 
14
54
  namespace schema {
15
55
  const Decimal = z.union([
@@ -98,8 +138,44 @@ namespace schema {
98
138
  ),
99
139
  })
100
140
 
141
+ export const Chains = z.object({
142
+ chains: z.array(
143
+ z.object({
144
+ id: Integer,
145
+ protocol: z.optional(
146
+ z.object({
147
+ v2: z.optional(
148
+ z.object({
149
+ depository: z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/)),
150
+ }),
151
+ ),
152
+ }),
153
+ ),
154
+ solverAddresses: z.optional(z.array(z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/)))),
155
+ }),
156
+ ),
157
+ })
158
+
159
+ export const DeliveryObservation = z.object({
160
+ relay: z.object({
161
+ orderId: z.string().check(z.regex(/^0x[0-9a-fA-F]{64}$/)),
162
+ sourceDepository: z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/)),
163
+ sourceObservedAt: z.iso.datetime(),
164
+ sourceSweepTransactionHash: z.string().check(z.regex(/^0x[0-9a-fA-F]{64}$/)),
165
+ }),
166
+ })
167
+
168
+ export const RequestCorrelation = z.object({
169
+ relay: z.object({
170
+ depositor: z.optional(z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/))),
171
+ depository: z.optional(z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/))),
172
+ orderId: z.optional(z.string().check(z.regex(/^0x[0-9a-fA-F]{64}$/))),
173
+ solver: z.optional(z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/))),
174
+ }),
175
+ })
176
+
101
177
  const RequestTransaction = z.object({
102
- hash: z.string().check(z.minLength(1)),
178
+ txHash: z.string().check(z.minLength(1)),
103
179
  })
104
180
 
105
181
  export const Requests = z.object({
@@ -113,11 +189,35 @@ namespace schema {
113
189
  }),
114
190
  ),
115
191
  depositAddress: z.optional(
116
- z.object({
117
- depositTxHash: z.optional(z.string().check(z.minLength(1))),
118
- }),
192
+ z.nullable(
193
+ z.object({
194
+ depositTxHash: z.optional(z.nullable(z.string().check(z.minLength(1)))),
195
+ }),
196
+ ),
119
197
  ),
120
198
  id: z.string().check(z.minLength(1)),
199
+ protocol: z.optional(
200
+ z.nullable(
201
+ z.object({
202
+ deposit: z.optional(
203
+ z.object({
204
+ origin: z.optional(
205
+ z.object({
206
+ depositor: z.optional(z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/))),
207
+ depository: z.optional(z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/))),
208
+ }),
209
+ ),
210
+ }),
211
+ ),
212
+ orderId: z.optional(z.string().check(z.regex(/^0x[0-9a-fA-F]{64}$/))),
213
+ solver: z.optional(
214
+ z.object({
215
+ address: z.optional(z.string().check(z.regex(/^0x[0-9a-fA-F]{40}$/))),
216
+ }),
217
+ ),
218
+ }),
219
+ ),
220
+ ),
121
221
  status: z.string().check(z.minLength(1)),
122
222
  updatedAt: z.iso.datetime(),
123
223
  }),
@@ -128,11 +228,20 @@ namespace schema {
128
228
  data: z.object({
129
229
  depositAddress: z.nullable(z.object({ address: z.string().check(z.minLength(1)) })),
130
230
  requestId: z.optional(z.string().check(z.minLength(1))),
231
+ status: z.string().check(z.minLength(1)),
232
+ updatedAt: z.number().check(z.int(), z.nonnegative()),
131
233
  }),
132
234
  event: z.literal('request.status.updated'),
133
235
  })
134
236
  }
135
237
 
238
+ /** Validates a Relay request-list response and reports payload drift with field-level details. */
239
+ export function parseDepositAddressRequests(value: unknown) {
240
+ const parsed = schema.Requests.safeParse(value)
241
+ if (!parsed.success) throw new FundingProvider.ProviderPayloadError(z.prettifyError(parsed.error))
242
+ return parsed.data
243
+ }
244
+
136
245
  /** Creates a Relay funding provider backed by `/quote/v2`. */
137
246
  export function relay(options: relay.Options = {}): FundingProvider.Provider<'relay'> {
138
247
  if (
@@ -146,6 +255,15 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
146
255
  const endpoint = quoteEndpoint(options)
147
256
  const fetch = options.fetch ?? defaultFetch
148
257
  const now = options.now ?? (() => new Date())
258
+ const observeDepositAddressRequest = options.observation
259
+ ? createDepositAddressObserver({
260
+ ...options.observation,
261
+ ...(options.apiKey ? { apiKey: options.apiKey } : {}),
262
+ ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),
263
+ fetch,
264
+ now,
265
+ })
266
+ : undefined
149
267
  const webhook = (() => {
150
268
  if (!options.apiKey || !options.webhook) return undefined
151
269
  const { apiKey, webhook } = options
@@ -153,6 +271,7 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
153
271
  verifyWebhook(webhookOptions(input, apiKey))
154
272
  return {
155
273
  async receive(input: FundingProvider.Webhook.receive.Parameters) {
274
+ const receivedAt = now().toISOString()
156
275
  const hint = (() => {
157
276
  try {
158
277
  return verify(input)
@@ -169,7 +288,8 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
169
288
  webhook.onResult?.('ignored')
170
289
  return { type: 'ignored' as const }
171
290
  }
172
- const address = await FundingDepositAddresses.getByProviderAddress(Db.get(webhook.db), {
291
+ const database = Db.get(webhook.db)
292
+ const address = await FundingDepositAddresses.getByProviderAddress(database, {
173
293
  address: hint.address,
174
294
  providerId: 'relay',
175
295
  })
@@ -177,11 +297,33 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
177
297
  webhook.onResult?.('unknown_address')
178
298
  return { type: 'unknown_address' as const }
179
299
  }
300
+ if (hint.requestId)
301
+ await FundingDepositRequestObservations.observeWebhook(database, {
302
+ depositAddressId: address.id,
303
+ providerRequestId: hint.requestId,
304
+ receivedAt,
305
+ }).catch((cause) => {
306
+ webhook.onResult?.('observation_failed')
307
+ throw cause
308
+ })
180
309
  try {
310
+ const webhookTiming = (() => {
311
+ if (!hint.providerStatus || !hint.providerUpdatedAt || !hint.requestId || !hint.sentAt)
312
+ return undefined
313
+ return {
314
+ enqueuedAt: now().toISOString(),
315
+ providerStatus: hint.providerStatus,
316
+ providerUpdatedAt: hint.providerUpdatedAt,
317
+ receivedAt,
318
+ sentAt: hint.sentAt,
319
+ }
320
+ })()
181
321
  await webhook.dispatch({
182
322
  addressId: address.id,
183
323
  ...(hint.requestId ? { requestId: hint.requestId } : {}),
324
+ trigger: 'webhook',
184
325
  type: 'funding:deposit-address:reconcile',
326
+ ...(webhookTiming ? { webhookTiming } : {}),
185
327
  })
186
328
  } catch (cause) {
187
329
  webhook.onResult?.('enqueue_failed')
@@ -323,10 +465,8 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
323
465
  async listDepositAddressRequests({ address, continuation, requestId }, signal) {
324
466
  const requestUrl = requestsEndpoint(options)
325
467
  if (requestId) requestUrl.searchParams.set('id', requestId)
326
- else {
327
- requestUrl.searchParams.set('depositAddress', address)
328
- requestUrl.searchParams.set('includeChildRequests', 'true')
329
- }
468
+ else requestUrl.searchParams.set('depositAddress', address)
469
+ requestUrl.searchParams.set('includeChildRequests', 'true')
330
470
  requestUrl.searchParams.set('limit', '50')
331
471
  requestUrl.searchParams.set('sortBy', 'updatedAt')
332
472
  requestUrl.searchParams.set('sortDirection', 'desc')
@@ -337,22 +477,41 @@ export function relay(options: relay.Options = {}): FundingProvider.Provider<'re
337
477
  method: 'GET',
338
478
  signal,
339
479
  })
340
- const parsed = schema.Requests.safeParse(response)
341
- if (!parsed.success) throw new FundingProvider.ProviderPayloadError()
480
+ const parsed = parseDepositAddressRequests(response)
342
481
  return {
343
- ...(parsed.data.continuation ? { continuation: parsed.data.continuation } : {}),
344
- requests: parsed.data.requests.map((request) => ({
345
- createdAt: new Date(request.createdAt).toISOString(),
346
- destinationTransactionHashes: request.data?.outTxs?.map((tx) => tx.hash) ?? [],
347
- id: request.id,
348
- sourceTransactionHashes: request.depositAddress?.depositTxHash
349
- ? [request.depositAddress.depositTxHash]
350
- : [],
351
- status: request.status,
352
- updatedAt: new Date(request.updatedAt).toISOString(),
353
- })),
482
+ ...(parsed.continuation ? { continuation: parsed.continuation } : {}),
483
+ requests: parsed.requests.map((request) => {
484
+ const correlation = (() => {
485
+ const { protocol } = request
486
+ if (!protocol) return undefined
487
+ const relay = {
488
+ ...(protocol.deposit?.origin?.depositor
489
+ ? { depositor: protocol.deposit.origin.depositor }
490
+ : {}),
491
+ ...(protocol.deposit?.origin?.depository
492
+ ? { depository: protocol.deposit.origin.depository }
493
+ : {}),
494
+ ...(protocol.orderId ? { orderId: protocol.orderId } : {}),
495
+ ...(protocol.solver?.address ? { solver: protocol.solver.address } : {}),
496
+ }
497
+ if (Object.keys(relay).length === 0) return undefined
498
+ return schema.RequestCorrelation.parse({ relay })
499
+ })()
500
+ return {
501
+ createdAt: new Date(request.createdAt).toISOString(),
502
+ destinationTransactionHashes: request.data?.outTxs?.map((tx) => tx.txHash) ?? [],
503
+ id: request.id,
504
+ ...(correlation ? { providerState: correlation } : {}),
505
+ sourceTransactionHashes: request.depositAddress?.depositTxHash
506
+ ? [request.depositAddress.depositTxHash]
507
+ : [],
508
+ status: request.status,
509
+ updatedAt: new Date(request.updatedAt).toISOString(),
510
+ }
511
+ }),
354
512
  }
355
513
  },
514
+ ...(observeDepositAddressRequest ? { observeDepositAddressRequest } : {}),
356
515
  async prepareTransfer({ candidate, method, mode, recipient, sender, slippageBps }, signal) {
357
516
  // Catalog capabilities gate deposit addresses and exact destination
358
517
  // off this adapter; refuse them defensively if one slips through.
@@ -445,6 +604,8 @@ export declare namespace relay {
445
604
  fetch?: FundingProvider.Fetch | undefined
446
605
  /** Clock override for deterministic tests. */
447
606
  now?: (() => Date) | undefined
607
+ /** EVM chain evidence used to accelerate authenticated provider delivery. */
608
+ observation?: Observation | undefined
448
609
  /** Optional recipient override; defaults to `userAddress`. */
449
610
  recipientAddress?: string | undefined
450
611
  /** Deposit subsidy policy enabled for this integration. */
@@ -455,6 +616,16 @@ export declare namespace relay {
455
616
  webhook?: Webhook | undefined
456
617
  }
457
618
 
619
+ /** Chain evidence configuration for Relay delivery observation. */
620
+ type Observation = {
621
+ /** Authoritative funding database or per-request database factory. */
622
+ db: Db.Source
623
+ /** Tempo chain that receives a shared RPC URL override. */
624
+ defaultChainId?: Viem.ChainId | undefined
625
+ /** Shared or per-EVM-chain RPC URL environment value. */
626
+ rpcUrlEnv?: Viem.UrlResolver | undefined
627
+ }
628
+
458
629
  /** Authenticated Relay webhook integration attached to a provider instance. */
459
630
  type Webhook = {
460
631
  /** Authoritative funding database or per-request database factory. */
@@ -470,11 +641,332 @@ export declare namespace relay {
470
641
  | 'authentication_failed'
471
642
  | 'enqueue_failed'
472
643
  | 'ignored'
644
+ | 'observation_failed'
473
645
  | 'payload_invalid'
474
646
  | 'queued'
475
647
  | 'unknown_address'
476
648
  }
477
649
 
650
+ type DepositAddressObserverOptions = relay.Observation & {
651
+ apiKey?: string | undefined
652
+ baseUrl?: string | undefined
653
+ fetch: FundingProvider.Fetch
654
+ now: () => Date
655
+ }
656
+
657
+ type RelayChain = z.output<typeof schema.Chains>['chains'][number]
658
+ type RelayDeliveryObservation = z.output<typeof schema.DeliveryObservation>['relay']
659
+
660
+ function createDepositAddressObserver(
661
+ options: DepositAddressObserverOptions,
662
+ ): NonNullable<FundingProvider.Provider['observeDepositAddressRequest']> {
663
+ let chainsExpiresAt = 0
664
+ let chainsPromise: Promise<readonly RelayChain[]> | undefined
665
+ let urlsExpiresAt = 0
666
+ let urlsPromise: Promise<ReadonlyMap<string, readonly string[]>> | undefined
667
+ const getChains = (signal: AbortSignal) => {
668
+ if (chainsPromise && options.now().getTime() < chainsExpiresAt) return chainsPromise
669
+ chainsExpiresAt = options.now().getTime() + relayChainsCacheTtlMs
670
+ chainsPromise = FundingProvider.requestJson(chainsEndpoint(options), {
671
+ fetch: options.fetch,
672
+ headers: options.apiKey ? { 'x-api-key': options.apiKey } : undefined,
673
+ signal,
674
+ })
675
+ .then((value) => schema.Chains.parse(value).chains)
676
+ .catch((cause) => {
677
+ chainsExpiresAt = 0
678
+ chainsPromise = undefined
679
+ throw cause
680
+ })
681
+ return chainsPromise
682
+ }
683
+ const getUrls = () => {
684
+ if (urlsPromise && options.now().getTime() < urlsExpiresAt) return urlsPromise
685
+ urlsExpiresAt = options.now().getTime() + relayChainsCacheTtlMs
686
+ urlsPromise = FundingCatalog.read(Db.get(options.db))
687
+ .then(
688
+ (catalog) =>
689
+ new Map<string, readonly string[]>(
690
+ [...catalog.chainsByKey.values()].map(
691
+ (chain) =>
692
+ [
693
+ chain.id,
694
+ Evidence.resolveRpcUrls({
695
+ chain,
696
+ defaultChainId: options.defaultChainId ?? Viem.defaultChainId,
697
+ rpcUrlEnv: options.rpcUrlEnv,
698
+ }),
699
+ ] as const,
700
+ ),
701
+ ),
702
+ )
703
+ .catch((cause) => {
704
+ urlsExpiresAt = 0
705
+ urlsPromise = undefined
706
+ throw cause
707
+ })
708
+ return urlsPromise
709
+ }
710
+
711
+ return async (parameters, signal) => {
712
+ if (
713
+ parameters.request &&
714
+ !(deliveryObservationStatuses as readonly string[]).includes(parameters.request.status)
715
+ )
716
+ return { type: 'unsupported' }
717
+ if (parameters.sourceChain.kind !== 'evm' || parameters.destinationChain.kind !== 'evm')
718
+ return { type: 'unsupported' }
719
+ if (
720
+ !Address.validate(parameters.address, { strict: false }) ||
721
+ !Address.validate(parameters.destinationToken.address, { strict: false }) ||
722
+ !Address.validate(parameters.recipient, { strict: false }) ||
723
+ !Address.validate(parameters.refundAddress, { strict: false }) ||
724
+ !Address.validate(parameters.sourceToken.address, { strict: false }) ||
725
+ !Hash.validate(parameters.source.transactionHash)
726
+ )
727
+ return { type: 'unsupported' }
728
+
729
+ const [chains, urls] = await Promise.all([getChains(signal), getUrls()])
730
+ const sourceChain = relayEvmChain(chains, parameters.sourceChain)
731
+ const destinationChain = relayEvmChain(chains, parameters.destinationChain)
732
+ const sourceUrls = urls.get(parameters.sourceChain.id)
733
+ const destinationUrls = urls.get(parameters.destinationChain.id)
734
+ const depository = sourceChain?.protocol?.v2?.depository
735
+ const solvers = destinationChain?.solverAddresses
736
+ if (
737
+ !depository ||
738
+ !solvers ||
739
+ solvers.length === 0 ||
740
+ !sourceUrls?.length ||
741
+ !destinationUrls?.length
742
+ )
743
+ return { type: 'unsupported' }
744
+
745
+ const parsedCorrelation = schema.RequestCorrelation.safeParse(parameters.request?.providerState)
746
+ const correlation = parsedCorrelation.success ? parsedCorrelation.data.relay : undefined
747
+ if (
748
+ (correlation?.depository &&
749
+ !Address.isEqual(
750
+ correlation.depository as Address.Address,
751
+ depository as Address.Address,
752
+ )) ||
753
+ (correlation?.solver &&
754
+ !solvers.some((solver) =>
755
+ Address.isEqual(solver as Address.Address, correlation.solver as Address.Address),
756
+ ))
757
+ )
758
+ return { type: 'unsupported' }
759
+ const depositor = (correlation?.depositor ?? parameters.refundAddress) as Address.Address
760
+
761
+ const stored = schema.DeliveryObservation.safeParse(parameters.providerState)
762
+ const sourceObservation = await (async () => {
763
+ if (stored.success) return stored.data.relay
764
+ const sourceClient = jsonRpcClient({ fetch: options.fetch, urls: sourceUrls })
765
+ const sourceReceipt = await getTransactionReceipt(sourceClient, {
766
+ hash: parameters.source.transactionHash as Hex.Hex,
767
+ })
768
+ if (sourceReceipt.status !== 'success' || sourceReceipt.blockNumber === null) return undefined
769
+ const sourceHead = await getBlockNumber(sourceClient, { cacheTime: 0 })
770
+ const sweepLogs = await getLogs(sourceClient, {
771
+ address: depository as Address.Address,
772
+ event: relayErc20DepositEvent,
773
+ fromBlock: sourceReceipt.blockNumber,
774
+ toBlock: minimum(sourceHead, sourceReceipt.blockNumber + sourceSweepForwardBlocks),
775
+ })
776
+ const observations: RelayDeliveryObservation[] = []
777
+ for (const log of sweepLogs) {
778
+ if (!log.transactionHash) continue
779
+ const event = decodeEvent(relayErc20DepositEvent, log)
780
+ if (
781
+ !event ||
782
+ !Address.isEqual(event.from, depositor) ||
783
+ !Address.isEqual(event.token, parameters.sourceToken.address as Address.Address) ||
784
+ event.amount !== BigInt(parameters.source.amount) ||
785
+ (correlation?.orderId && !Hex.isEqual(event.id, correlation.orderId as Hex.Hex))
786
+ )
787
+ continue
788
+ const transaction = await getTransaction(sourceClient, { hash: log.transactionHash })
789
+ const call = decodeRelayErc20Deposit(transaction.input)
790
+ if (
791
+ !call ||
792
+ !Address.isEqual(transaction.from, parameters.address as Address.Address) ||
793
+ !transaction.to ||
794
+ !Address.isEqual(transaction.to, depository as Address.Address) ||
795
+ !Address.isEqual(call.depositor, event.from) ||
796
+ !Address.isEqual(call.token, event.token) ||
797
+ (call.amount !== undefined && call.amount !== event.amount) ||
798
+ !Hex.isEqual(call.id, event.id)
799
+ )
800
+ continue
801
+ observations.push({
802
+ orderId: event.id,
803
+ sourceDepository: depository,
804
+ sourceObservedAt: options.now().toISOString(),
805
+ sourceSweepTransactionHash: log.transactionHash,
806
+ })
807
+ }
808
+ return observations.length === 1 ? observations[0] : undefined
809
+ })()
810
+ if (!sourceObservation) return { type: 'pending' }
811
+
812
+ const destinationClient = jsonRpcClient({ fetch: options.fetch, urls: destinationUrls })
813
+ const destinationHead = await getBlockNumber(destinationClient, { cacheTime: 0 })
814
+ const destinationLogs = await getLogs(destinationClient, {
815
+ address: parameters.destinationToken.address as Address.Address,
816
+ args: { to: parameters.recipient as Address.Address },
817
+ event: transferEvent,
818
+ fromBlock:
819
+ destinationHead > deliveryBackfillBlocks ? destinationHead - deliveryBackfillBlocks : 0n,
820
+ toBlock: destinationHead,
821
+ })
822
+ const solverAddresses = (correlation?.solver ? [correlation.solver] : solvers).map(
823
+ (address) => address as Address.Address,
824
+ )
825
+ const correlatedTransactionHashes: Hex.Hex[] = []
826
+ const uncorrelatedTransactionHashes: Hex.Hex[] = []
827
+ for (const log of destinationLogs) {
828
+ if (
829
+ !log.transactionHash ||
830
+ correlatedTransactionHashes.includes(log.transactionHash) ||
831
+ uncorrelatedTransactionHashes.includes(log.transactionHash)
832
+ )
833
+ continue
834
+ const event = decodeEvent(transferEvent, log)
835
+ if (
836
+ !event ||
837
+ event.value === 0n ||
838
+ !Address.isEqual(event.to, parameters.recipient as Address.Address) ||
839
+ !solverAddresses.some((address) => Address.isEqual(address, event.from))
840
+ )
841
+ continue
842
+ const transaction = await getTransaction(destinationClient, { hash: log.transactionHash })
843
+ const call = decodeRelayTransfer(transaction.input)
844
+ if (
845
+ !call ||
846
+ !solverAddresses.some((address) => Address.isEqual(address, transaction.from)) ||
847
+ !Address.isEqual(event.from, transaction.from) ||
848
+ !transaction.to ||
849
+ !Address.isEqual(transaction.to, parameters.destinationToken.address as Address.Address) ||
850
+ !Address.isEqual(call.recipient, event.to) ||
851
+ call.amount !== event.value ||
852
+ (call.orderId && !Hex.isEqual(call.orderId, sourceObservation.orderId as Hex.Hex))
853
+ )
854
+ continue
855
+ if (call.orderId) correlatedTransactionHashes.push(log.transactionHash)
856
+ else uncorrelatedTransactionHashes.push(log.transactionHash)
857
+ }
858
+ const destinationTransactionHashes = (() => {
859
+ if (correlatedTransactionHashes.length > 0) return correlatedTransactionHashes
860
+ // Provider-independent completion requires the Relay order id to bind both chain legs.
861
+ if (!parameters.request) return []
862
+ if (uncorrelatedTransactionHashes.length === 1) return uncorrelatedTransactionHashes
863
+ return []
864
+ })()
865
+ const providerState = { relay: sourceObservation }
866
+ if (destinationTransactionHashes.length === 0)
867
+ return {
868
+ providerState,
869
+ providerTransactionHashes: [sourceObservation.sourceSweepTransactionHash],
870
+ type: 'pending',
871
+ }
872
+
873
+ return {
874
+ destinationTransactionHashes,
875
+ providerState: {
876
+ relay: {
877
+ ...sourceObservation,
878
+ destinationObservedAt: options.now().toISOString(),
879
+ },
880
+ },
881
+ providerTransactionHashes: [
882
+ sourceObservation.sourceSweepTransactionHash,
883
+ ...destinationTransactionHashes,
884
+ ],
885
+ type: 'verified',
886
+ }
887
+ }
888
+ }
889
+
890
+ function chainsEndpoint(options: Pick<DepositAddressObserverOptions, 'baseUrl'>): URL {
891
+ const endpoint = new URL(options.baseUrl ?? defaultBaseUrl)
892
+ endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/chains`
893
+ return endpoint
894
+ }
895
+
896
+ function decodeRelayErc20Deposit(data: Hex.Hex) {
897
+ const fullAllowanceLength = 2 + 2 * (4 + 32 * 3)
898
+ const withAmountLength = 2 + 2 * (4 + 32 * 4)
899
+ try {
900
+ if (data.length === withAmountLength) {
901
+ const [depositor, token, amount, id] = AbiFunction.decodeData(depositErc20WithAmount, data)
902
+ return { amount, depositor, id, token }
903
+ }
904
+ if (data.length === fullAllowanceLength) {
905
+ const [depositor, token, id] = AbiFunction.decodeData(depositErc20FullAllowance, data)
906
+ return { depositor, id, token }
907
+ }
908
+ return undefined
909
+ } catch {
910
+ return undefined
911
+ }
912
+ }
913
+
914
+ function decodeRelayTransfer(data: Hex.Hex) {
915
+ const encodedLength = 2 + 2 * (4 + 32 * 2)
916
+ if (data.length !== encodedLength && data.length !== encodedLength + 64) return undefined
917
+ try {
918
+ const [recipient, amount] = AbiFunction.decodeData(
919
+ relayTransfer,
920
+ data.slice(0, encodedLength) as Hex.Hex,
921
+ )
922
+ return {
923
+ amount,
924
+ ...(data.length > encodedLength
925
+ ? { orderId: `0x${data.slice(encodedLength)}` as Hex.Hex }
926
+ : {}),
927
+ recipient,
928
+ }
929
+ } catch {
930
+ return undefined
931
+ }
932
+ }
933
+
934
+ function decodeEvent<const event extends AbiEvent.AbiEvent>(
935
+ abiEvent: event,
936
+ log: { data: Hex.Hex; topics: readonly Hex.Hex[] },
937
+ ): AbiEvent.decode.ReturnType<event> | undefined {
938
+ try {
939
+ return AbiEvent.decode(abiEvent, log)
940
+ } catch {
941
+ return undefined
942
+ }
943
+ }
944
+
945
+ function jsonRpcClient(options: { fetch: FundingProvider.Fetch; urls: readonly string[] }) {
946
+ return createClient({
947
+ transport: fallback(
948
+ options.urls.map((url) => {
949
+ const transport = RpcTransport.fromHttp(url, { fetchFn: options.fetch })
950
+ return custom(transport, { retryCount: 0 })
951
+ }),
952
+ ),
953
+ })
954
+ }
955
+
956
+ function minimum(left: bigint, right: bigint): bigint {
957
+ return left < right ? left : right
958
+ }
959
+
960
+ function relayEvmChain(
961
+ chains: readonly RelayChain[],
962
+ chain: FundingProvider.ChainRef,
963
+ ): RelayChain | undefined {
964
+ if (chain.kind !== 'evm') return undefined
965
+ const match = /^eip155:(\d+)$/.exec(chain.id)
966
+ if (!match?.[1]) return undefined
967
+ return chains.find((candidate) => candidate.id === Number(match[1]))
968
+ }
969
+
478
970
  type VerifyWebhookOptions = {
479
971
  body: string
480
972
  secret: string
@@ -485,8 +977,7 @@ type VerifyWebhookOptions = {
485
977
  function verifyWebhook(options: VerifyWebhookOptions): FundingProvider.Webhook.verify.ReturnType {
486
978
  const timestamp = Number(options.timestamp)
487
979
  if (!Number.isSafeInteger(timestamp)) throw new WebhookSignatureError()
488
- const now = Math.floor(Date.now() / 1_000)
489
- if (Math.abs(now - timestamp) > webhookSignatureToleranceSeconds)
980
+ if (Math.abs(Date.now() - timestamp) > webhookSignatureToleranceMs)
490
981
  throw new WebhookSignatureError()
491
982
  if (!/^[0-9a-fA-F]{64}$/.test(options.signature)) throw new WebhookSignatureError()
492
983
  const expected = Hash.hmac256(
@@ -506,7 +997,10 @@ function verifyWebhook(options: VerifyWebhookOptions): FundingProvider.Webhook.v
506
997
  if (!parsed.success) throw new WebhookPayloadError()
507
998
  return {
508
999
  address: parsed.data.data.depositAddress?.address,
1000
+ providerStatus: webhookStatus(parsed.data.data.status),
1001
+ providerUpdatedAt: webhookTimestamp(parsed.data.data.updatedAt),
509
1002
  requestId: parsed.data.data.requestId,
1003
+ sentAt: webhookTimestamp(timestamp),
510
1004
  }
511
1005
  }
512
1006
 
@@ -528,7 +1022,7 @@ function quoteEndpoint(options: relay.Options): URL {
528
1022
 
529
1023
  function requestsEndpoint(options: relay.Options): URL {
530
1024
  const endpoint = new URL(options.baseUrl ?? defaultBaseUrl)
531
- endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/requests/v2`
1025
+ endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/requests/v3`
532
1026
  return endpoint
533
1027
  }
534
1028
 
@@ -540,6 +1034,18 @@ function timingSafeEqual(left: string, right: string): boolean {
540
1034
  return mismatch === 0
541
1035
  }
542
1036
 
1037
+ function webhookStatus(status: string): string {
1038
+ return (webhookStatuses as readonly string[]).includes(status) ? status : 'unknown'
1039
+ }
1040
+
1041
+ function webhookTimestamp(value: number): string {
1042
+ try {
1043
+ return new Date(value).toISOString()
1044
+ } catch {
1045
+ throw new WebhookPayloadError()
1046
+ }
1047
+ }
1048
+
543
1049
  function relayChainId(chain: FundingProvider.Chain): number {
544
1050
  if (chain.id.startsWith('eip155:')) return Number(FundingChain.eip155Id(chain))
545
1051
  // Relay uses a numeric chain ID for Solana instead of its CAIP-2 reference.