tapimo 0.7.2 → 0.8.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 (294) 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 +20434 -19484
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +62 -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 +168 -168
  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 +10305 -12785
  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 +24 -24
  35. package/dist/apps/data/routes/addresses.d.ts +288 -288
  36. package/dist/apps/data/routes/balances.d.ts +6 -6
  37. package/dist/apps/data/routes/blocks.d.ts +39 -39
  38. package/dist/apps/data/routes/coingecko.d.ts +170 -170
  39. package/dist/apps/data/routes/earn.d.ts +436 -6
  40. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  41. package/dist/apps/data/routes/earn.js +631 -4
  42. package/dist/apps/data/routes/earn.js.map +1 -1
  43. package/dist/apps/data/routes/exchanges.d.ts +10 -10
  44. package/dist/apps/data/routes/fee-amm.d.ts +2 -2
  45. package/dist/apps/data/routes/indexer.d.ts +6 -6
  46. package/dist/apps/data/routes/indexer.js +4 -1
  47. package/dist/apps/data/routes/indexer.js.map +1 -1
  48. package/dist/apps/data/routes/receipts.d.ts +235 -235
  49. package/dist/apps/data/routes/rpc.d.ts +1 -1
  50. package/dist/apps/data/routes/rpc.js +4 -1
  51. package/dist/apps/data/routes/rpc.js.map +1 -1
  52. package/dist/apps/data/routes/tokenlist.d.ts +36 -36
  53. package/dist/apps/data/routes/tokens.d.ts +6 -6
  54. package/dist/apps/data/routes/transactions.d.ts +2 -2
  55. package/dist/apps/data/routes/transfers.d.ts +1 -1
  56. package/dist/apps/data/routes/valuation.d.ts +1 -1
  57. package/dist/apps/data/routes/verified-tokens.d.ts +36 -36
  58. package/dist/apps/data/routes/webhooks.d.ts +9 -9
  59. package/dist/apps/data/routes/zones.d.ts +14 -14
  60. package/dist/apps/funding/Access.d.ts +16 -0
  61. package/dist/apps/funding/Access.d.ts.map +1 -0
  62. package/dist/apps/funding/Access.js +22 -0
  63. package/dist/apps/funding/Access.js.map +1 -0
  64. package/dist/apps/{data/routes/funding.d.ts → funding/App.d.ts} +1717 -3035
  65. package/dist/apps/funding/App.d.ts.map +1 -0
  66. package/dist/apps/funding/App.js +46 -0
  67. package/dist/apps/funding/App.js.map +1 -0
  68. package/dist/apps/funding/Inventory.d.ts +5 -0
  69. package/dist/apps/funding/Inventory.d.ts.map +1 -0
  70. package/dist/apps/funding/Inventory.js +25 -0
  71. package/dist/apps/funding/Inventory.js.map +1 -0
  72. package/dist/apps/funding/ProviderAttempts.d.ts +5 -0
  73. package/dist/apps/funding/ProviderAttempts.d.ts.map +1 -0
  74. package/dist/apps/funding/ProviderAttempts.js +24 -0
  75. package/dist/apps/funding/ProviderAttempts.js.map +1 -0
  76. package/dist/apps/funding/index.d.ts +2 -0
  77. package/dist/apps/funding/index.d.ts.map +1 -0
  78. package/dist/apps/funding/index.js +2 -0
  79. package/dist/apps/funding/index.js.map +1 -0
  80. package/dist/apps/funding/routes/chains.d.ts +169 -0
  81. package/dist/apps/funding/routes/chains.d.ts.map +1 -0
  82. package/dist/apps/funding/routes/chains.js +80 -0
  83. package/dist/apps/funding/routes/chains.js.map +1 -0
  84. package/dist/apps/funding/routes/deposit-addresses.d.ts +2388 -0
  85. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -0
  86. package/dist/apps/funding/routes/deposit-addresses.js +860 -0
  87. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -0
  88. package/dist/apps/funding/routes/providers.d.ts +91 -0
  89. package/dist/apps/funding/routes/providers.d.ts.map +1 -0
  90. package/dist/apps/funding/routes/providers.js +56 -0
  91. package/dist/apps/funding/routes/providers.js.map +1 -0
  92. package/dist/apps/funding/routes/quotes.d.ts +426 -0
  93. package/dist/apps/funding/routes/quotes.d.ts.map +1 -0
  94. package/dist/apps/funding/routes/quotes.js +188 -0
  95. package/dist/apps/funding/routes/quotes.js.map +1 -0
  96. package/dist/apps/funding/routes/transfers.d.ts +1999 -0
  97. package/dist/apps/funding/routes/transfers.d.ts.map +1 -0
  98. package/dist/apps/funding/routes/transfers.js +614 -0
  99. package/dist/apps/funding/routes/transfers.js.map +1 -0
  100. package/dist/apps/index.d.ts +1 -0
  101. package/dist/apps/index.d.ts.map +1 -1
  102. package/dist/apps/index.js +1 -0
  103. package/dist/apps/index.js.map +1 -1
  104. package/dist/apps/management/App.d.ts +430 -165
  105. package/dist/apps/management/App.d.ts.map +1 -1
  106. package/dist/apps/management/routes/api-keys.d.ts +298 -21
  107. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  108. package/dist/apps/management/routes/api-keys.js +75 -1
  109. package/dist/apps/management/routes/api-keys.js.map +1 -1
  110. package/dist/apps/management/routes/billing.d.ts +6 -6
  111. package/dist/apps/management/routes/faucet.d.ts +35 -35
  112. package/dist/apps/management/routes/invitations.d.ts +48 -48
  113. package/dist/apps/management/routes/invite-links.d.ts +38 -38
  114. package/dist/apps/management/routes/members.d.ts +69 -69
  115. package/dist/apps/management/routes/orgs.d.ts +37 -37
  116. package/dist/apps/management/routes/projects.d.ts +25 -25
  117. package/dist/apps/management/routes/scopes.d.ts +1 -1
  118. package/dist/apps/management/routes/usage.d.ts +72 -72
  119. package/dist/apps/management/routes/verified-token-requests.d.ts +18 -18
  120. package/dist/apps/management/routes/webhooks.d.ts +128 -128
  121. package/dist/apps/mcp.d.ts +2 -3
  122. package/dist/apps/mcp.d.ts.map +1 -1
  123. package/dist/apps/mcp.js +16 -14
  124. package/dist/apps/mcp.js.map +1 -1
  125. package/dist/apps/metadata.d.ts +4 -0
  126. package/dist/apps/metadata.d.ts.map +1 -1
  127. package/dist/apps/metadata.js +10 -0
  128. package/dist/apps/metadata.js.map +1 -1
  129. package/dist/apps/mpp/App.d.ts +2 -2
  130. package/dist/cloudflare.d.ts +50 -0
  131. package/dist/cloudflare.d.ts.map +1 -1
  132. package/dist/cloudflare.js +178 -0
  133. package/dist/cloudflare.js.map +1 -1
  134. package/dist/db/Db.d.ts +2 -0
  135. package/dist/db/Db.d.ts.map +1 -1
  136. package/dist/db/Db.js +37 -0
  137. package/dist/db/Db.js.map +1 -1
  138. package/dist/db/Schema.d.ts +12 -0
  139. package/dist/db/Schema.d.ts.map +1 -1
  140. package/dist/db/Schema.js +20 -0
  141. package/dist/db/Schema.js.map +1 -1
  142. package/dist/db/tables/fundingDepositAddresses.d.ts +13 -0
  143. package/dist/db/tables/fundingDepositAddresses.d.ts.map +1 -1
  144. package/dist/db/tables/fundingDepositAddresses.js +12 -0
  145. package/dist/db/tables/fundingDepositAddresses.js.map +1 -1
  146. package/dist/db/tables/fundingDepositRequestObservations.d.ts +45 -0
  147. package/dist/db/tables/fundingDepositRequestObservations.d.ts.map +1 -0
  148. package/dist/db/tables/fundingDepositRequestObservations.js +46 -0
  149. package/dist/db/tables/fundingDepositRequestObservations.js.map +1 -0
  150. package/dist/db/tables/fundingDeposits.d.ts +57 -1
  151. package/dist/db/tables/fundingDeposits.d.ts.map +1 -1
  152. package/dist/db/tables/fundingDeposits.js +82 -0
  153. package/dist/db/tables/fundingDeposits.js.map +1 -1
  154. package/dist/internal/Auth.d.ts +11 -11
  155. package/dist/internal/Auth.d.ts.map +1 -1
  156. package/dist/internal/Auth.js +41 -15
  157. package/dist/internal/Auth.js.map +1 -1
  158. package/dist/internal/Cache.js +1 -1
  159. package/dist/internal/Cache.js.map +1 -1
  160. package/dist/internal/EdgeCache.d.ts +2 -2
  161. package/dist/internal/EdgeCache.d.ts.map +1 -1
  162. package/dist/internal/EdgeCache.js +10 -4
  163. package/dist/internal/EdgeCache.js.map +1 -1
  164. package/dist/internal/IpAllowlist.d.ts +15 -0
  165. package/dist/internal/IpAllowlist.d.ts.map +1 -0
  166. package/dist/internal/IpAllowlist.js +27 -0
  167. package/dist/internal/IpAllowlist.js.map +1 -0
  168. package/dist/internal/OpenApi.d.ts +1 -1
  169. package/dist/internal/OpenApi.js +1 -1
  170. package/dist/internal/OpenApi.js.map +1 -1
  171. package/dist/internal/Tidx.d.ts +6 -0
  172. package/dist/internal/Tidx.d.ts.map +1 -1
  173. package/dist/internal/Tidx.js +13 -3
  174. package/dist/internal/Tidx.js.map +1 -1
  175. package/dist/internal/Viem.d.ts +8 -0
  176. package/dist/internal/Viem.d.ts.map +1 -1
  177. package/dist/internal/Viem.js +12 -5
  178. package/dist/internal/Viem.js.map +1 -1
  179. package/dist/internal/funding/Deposit.d.ts +42 -0
  180. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  181. package/dist/internal/funding/Deposit.js +37 -5
  182. package/dist/internal/funding/Deposit.js.map +1 -1
  183. package/dist/internal/funding/Evidence.d.ts +1 -5
  184. package/dist/internal/funding/Evidence.d.ts.map +1 -1
  185. package/dist/internal/funding/Evidence.js +4 -3
  186. package/dist/internal/funding/Evidence.js.map +1 -1
  187. package/dist/internal/funding/Provider.d.ts +102 -0
  188. package/dist/internal/funding/Provider.d.ts.map +1 -1
  189. package/dist/internal/funding/Provider.js +5 -0
  190. package/dist/internal/funding/Provider.js.map +1 -1
  191. package/dist/internal/funding/Reconciliation.d.ts +16 -2
  192. package/dist/internal/funding/Reconciliation.d.ts.map +1 -1
  193. package/dist/internal/funding/Reconciliation.js +257 -48
  194. package/dist/internal/funding/Reconciliation.js.map +1 -1
  195. package/dist/internal/funding/SourceObservation.d.ts +108 -0
  196. package/dist/internal/funding/SourceObservation.d.ts.map +1 -0
  197. package/dist/internal/funding/SourceObservation.js +143 -0
  198. package/dist/internal/funding/SourceObservation.js.map +1 -0
  199. package/dist/internal/funding/Subsidy.d.ts +2 -2
  200. package/dist/internal/funding/Subsidy.d.ts.map +1 -1
  201. package/dist/internal/funding/Subsidy.js +1 -1
  202. package/dist/internal/funding/Subsidy.js.map +1 -1
  203. package/dist/internal/funding/index.d.ts +1 -0
  204. package/dist/internal/funding/index.d.ts.map +1 -1
  205. package/dist/internal/funding/index.js +1 -0
  206. package/dist/internal/funding/index.js.map +1 -1
  207. package/dist/internal/funding/providers/relay.d.ts +43 -1
  208. package/dist/internal/funding/providers/relay.d.ts.map +1 -1
  209. package/dist/internal/funding/providers/relay.js +434 -27
  210. package/dist/internal/funding/providers/relay.js.map +1 -1
  211. package/package.json +3 -3
  212. package/src/ApiKey.test.ts +35 -0
  213. package/src/ApiKey.ts +27 -0
  214. package/src/ApiKeys.test.ts +29 -0
  215. package/src/ApiKeys.ts +11 -3
  216. package/src/App.test.ts +23 -5
  217. package/src/App.ts +76 -5
  218. package/src/Client.test-d.ts +171 -19
  219. package/src/admin/apps/api-keys.test.ts +25 -1
  220. package/src/admin/apps/api-keys.ts +6 -2
  221. package/src/admin/ui.generated.ts +67 -67
  222. package/src/analytics/Analytics.ts +4 -0
  223. package/src/apps/composition.test.ts +100 -2
  224. package/src/apps/data/App.test.ts +0 -12
  225. package/src/apps/data/App.ts +4 -8
  226. package/src/apps/data/routes/earn.test.ts +283 -0
  227. package/src/apps/data/routes/earn.ts +800 -3
  228. package/src/apps/data/routes/indexer.ts +2 -1
  229. package/src/apps/data/routes/rpc.ts +2 -0
  230. package/src/apps/data/routes/tokens.test.ts +6 -8
  231. package/src/apps/funding/Access.ts +26 -0
  232. package/src/apps/funding/App.test.ts +78 -0
  233. package/src/apps/funding/App.ts +63 -0
  234. package/src/apps/funding/Inventory.ts +30 -0
  235. package/src/apps/funding/ProviderAttempts.ts +26 -0
  236. package/src/apps/funding/index.ts +1 -0
  237. package/src/apps/funding/routes/chains.test.ts +78 -0
  238. package/src/apps/funding/routes/chains.ts +101 -0
  239. package/src/apps/funding/routes/deposit-addresses.test.ts +1628 -0
  240. package/src/apps/funding/routes/deposit-addresses.ts +1015 -0
  241. package/src/apps/funding/routes/providers.test.ts +165 -0
  242. package/src/apps/funding/routes/providers.ts +73 -0
  243. package/src/apps/funding/routes/quotes.test.ts +511 -0
  244. package/src/apps/funding/routes/quotes.ts +205 -0
  245. package/src/apps/funding/routes/transfers.test.ts +1255 -0
  246. package/src/apps/funding/routes/transfers.ts +704 -0
  247. package/src/apps/index.ts +1 -0
  248. package/src/apps/management/routes/api-keys.test.ts +98 -0
  249. package/src/apps/management/routes/api-keys.ts +96 -1
  250. package/src/apps/mcp.test.ts +17 -2
  251. package/src/apps/mcp.ts +18 -13
  252. package/src/apps/metadata.ts +12 -0
  253. package/src/apps/mpp/internal/Policy.test.ts +7 -1
  254. package/src/cloudflare.ts +279 -0
  255. package/src/db/Db.ts +62 -0
  256. package/src/db/Schema.ts +24 -0
  257. package/src/db/tables/fundingDepositAddresses.ts +28 -0
  258. package/src/db/tables/fundingDepositRequestObservations.test.ts +106 -0
  259. package/src/db/tables/fundingDepositRequestObservations.ts +99 -0
  260. package/src/db/tables/fundingDeposits.test.ts +138 -0
  261. package/src/db/tables/fundingDeposits.ts +145 -1
  262. package/src/internal/Auth.test.ts +177 -5
  263. package/src/internal/Auth.ts +48 -18
  264. package/src/internal/Cache.test.ts +37 -3
  265. package/src/internal/Cache.ts +1 -1
  266. package/src/internal/EdgeCache.test.ts +7 -7
  267. package/src/internal/EdgeCache.ts +13 -4
  268. package/src/internal/IpAllowlist.test.ts +52 -0
  269. package/src/internal/IpAllowlist.ts +39 -0
  270. package/src/internal/Log.test.ts +7 -1
  271. package/src/internal/OpenApi.ts +1 -1
  272. package/src/internal/Tidx.test.ts +28 -0
  273. package/src/internal/Tidx.ts +19 -3
  274. package/src/internal/Viem.test.ts +28 -0
  275. package/src/internal/Viem.ts +21 -5
  276. package/src/internal/funding/Deposit.ts +74 -5
  277. package/src/internal/funding/Evidence.test.ts +2 -2
  278. package/src/internal/funding/Evidence.ts +4 -9
  279. package/src/internal/funding/Provider.ts +115 -0
  280. package/src/internal/funding/Reconciliation.local.test.ts +7 -0
  281. package/src/internal/funding/Reconciliation.test.ts +1052 -35
  282. package/src/internal/funding/Reconciliation.ts +328 -56
  283. package/src/internal/funding/SourceObservation.test.ts +301 -0
  284. package/src/internal/funding/SourceObservation.ts +267 -0
  285. package/src/internal/funding/Subsidy.ts +2 -2
  286. package/src/internal/funding/index.test.ts +1 -0
  287. package/src/internal/funding/index.ts +1 -0
  288. package/src/internal/funding/providers/relay.test.ts +94 -4
  289. package/src/internal/funding/providers/relay.ts +528 -27
  290. package/dist/apps/data/routes/funding.d.ts.map +0 -1
  291. package/dist/apps/data/routes/funding.js +0 -1557
  292. package/dist/apps/data/routes/funding.js.map +0 -1
  293. package/src/apps/data/routes/funding.test.ts +0 -2994
  294. 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,327 @@ 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 (!(deliveryObservationStatuses as readonly string[]).includes(parameters.request.status))
713
+ return { type: 'unsupported' }
714
+ if (parameters.sourceChain.kind !== 'evm' || parameters.destinationChain.kind !== 'evm')
715
+ return { type: 'unsupported' }
716
+ if (
717
+ !Address.validate(parameters.address, { strict: false }) ||
718
+ !Address.validate(parameters.destinationToken.address, { strict: false }) ||
719
+ !Address.validate(parameters.recipient, { strict: false }) ||
720
+ !Address.validate(parameters.refundAddress, { strict: false }) ||
721
+ !Address.validate(parameters.sourceToken.address, { strict: false }) ||
722
+ !Hash.validate(parameters.source.transactionHash)
723
+ )
724
+ return { type: 'unsupported' }
725
+
726
+ const [chains, urls] = await Promise.all([getChains(signal), getUrls()])
727
+ const sourceChain = relayEvmChain(chains, parameters.sourceChain)
728
+ const destinationChain = relayEvmChain(chains, parameters.destinationChain)
729
+ const sourceUrls = urls.get(parameters.sourceChain.id)
730
+ const destinationUrls = urls.get(parameters.destinationChain.id)
731
+ const depository = sourceChain?.protocol?.v2?.depository
732
+ const solvers = destinationChain?.solverAddresses
733
+ if (
734
+ !depository ||
735
+ !solvers ||
736
+ solvers.length === 0 ||
737
+ !sourceUrls?.length ||
738
+ !destinationUrls?.length
739
+ )
740
+ return { type: 'unsupported' }
741
+
742
+ const parsedCorrelation = schema.RequestCorrelation.safeParse(parameters.request.providerState)
743
+ const correlation = parsedCorrelation.success ? parsedCorrelation.data.relay : undefined
744
+ if (
745
+ (correlation?.depository &&
746
+ !Address.isEqual(
747
+ correlation.depository as Address.Address,
748
+ depository as Address.Address,
749
+ )) ||
750
+ (correlation?.solver &&
751
+ !solvers.some((solver) =>
752
+ Address.isEqual(solver as Address.Address, correlation.solver as Address.Address),
753
+ ))
754
+ )
755
+ return { type: 'unsupported' }
756
+ const depositor = (correlation?.depositor ?? parameters.refundAddress) as Address.Address
757
+
758
+ const stored = schema.DeliveryObservation.safeParse(parameters.providerState)
759
+ const sourceObservation = await (async () => {
760
+ if (stored.success) return stored.data.relay
761
+ const sourceClient = jsonRpcClient({ fetch: options.fetch, urls: sourceUrls })
762
+ const sourceReceipt = await getTransactionReceipt(sourceClient, {
763
+ hash: parameters.source.transactionHash as Hex.Hex,
764
+ })
765
+ if (sourceReceipt.status !== 'success' || sourceReceipt.blockNumber === null) return undefined
766
+ const sourceHead = await getBlockNumber(sourceClient, { cacheTime: 0 })
767
+ const sweepLogs = await getLogs(sourceClient, {
768
+ address: depository as Address.Address,
769
+ event: relayErc20DepositEvent,
770
+ fromBlock: sourceReceipt.blockNumber,
771
+ toBlock: minimum(sourceHead, sourceReceipt.blockNumber + sourceSweepForwardBlocks),
772
+ })
773
+ const observations: RelayDeliveryObservation[] = []
774
+ for (const log of sweepLogs) {
775
+ if (!log.transactionHash) continue
776
+ const event = decodeEvent(relayErc20DepositEvent, log)
777
+ if (
778
+ !event ||
779
+ !Address.isEqual(event.from, depositor) ||
780
+ !Address.isEqual(event.token, parameters.sourceToken.address as Address.Address) ||
781
+ event.amount !== BigInt(parameters.source.amount) ||
782
+ (correlation?.orderId && !Hex.isEqual(event.id, correlation.orderId as Hex.Hex))
783
+ )
784
+ continue
785
+ const transaction = await getTransaction(sourceClient, { hash: log.transactionHash })
786
+ const call = decodeRelayErc20Deposit(transaction.input)
787
+ if (
788
+ !call ||
789
+ !Address.isEqual(transaction.from, parameters.address as Address.Address) ||
790
+ !transaction.to ||
791
+ !Address.isEqual(transaction.to, depository as Address.Address) ||
792
+ !Address.isEqual(call.depositor, event.from) ||
793
+ !Address.isEqual(call.token, event.token) ||
794
+ (call.amount !== undefined && call.amount !== event.amount) ||
795
+ !Hex.isEqual(call.id, event.id)
796
+ )
797
+ continue
798
+ observations.push({
799
+ orderId: event.id,
800
+ sourceDepository: depository,
801
+ sourceObservedAt: options.now().toISOString(),
802
+ sourceSweepTransactionHash: log.transactionHash,
803
+ })
804
+ }
805
+ return observations.length === 1 ? observations[0] : undefined
806
+ })()
807
+ if (!sourceObservation) return { type: 'pending' }
808
+
809
+ const destinationClient = jsonRpcClient({ fetch: options.fetch, urls: destinationUrls })
810
+ const destinationHead = await getBlockNumber(destinationClient, { cacheTime: 0 })
811
+ const destinationLogs = await getLogs(destinationClient, {
812
+ address: parameters.destinationToken.address as Address.Address,
813
+ args: { to: parameters.recipient as Address.Address },
814
+ event: transferEvent,
815
+ fromBlock:
816
+ destinationHead > deliveryBackfillBlocks ? destinationHead - deliveryBackfillBlocks : 0n,
817
+ toBlock: destinationHead,
818
+ })
819
+ const solverAddresses = (correlation?.solver ? [correlation.solver] : solvers).map(
820
+ (address) => address as Address.Address,
821
+ )
822
+ const correlatedTransactionHashes: Hex.Hex[] = []
823
+ const uncorrelatedTransactionHashes: Hex.Hex[] = []
824
+ for (const log of destinationLogs) {
825
+ if (
826
+ !log.transactionHash ||
827
+ correlatedTransactionHashes.includes(log.transactionHash) ||
828
+ uncorrelatedTransactionHashes.includes(log.transactionHash)
829
+ )
830
+ continue
831
+ const event = decodeEvent(transferEvent, log)
832
+ if (
833
+ !event ||
834
+ event.value === 0n ||
835
+ !Address.isEqual(event.to, parameters.recipient as Address.Address) ||
836
+ !solverAddresses.some((address) => Address.isEqual(address, event.from))
837
+ )
838
+ continue
839
+ const transaction = await getTransaction(destinationClient, { hash: log.transactionHash })
840
+ const call = decodeRelayTransfer(transaction.input)
841
+ if (
842
+ !call ||
843
+ !solverAddresses.some((address) => Address.isEqual(address, transaction.from)) ||
844
+ !Address.isEqual(event.from, transaction.from) ||
845
+ !transaction.to ||
846
+ !Address.isEqual(transaction.to, parameters.destinationToken.address as Address.Address) ||
847
+ !Address.isEqual(call.recipient, event.to) ||
848
+ call.amount !== event.value ||
849
+ (call.orderId && !Hex.isEqual(call.orderId, sourceObservation.orderId as Hex.Hex))
850
+ )
851
+ continue
852
+ if (call.orderId) correlatedTransactionHashes.push(log.transactionHash)
853
+ else uncorrelatedTransactionHashes.push(log.transactionHash)
854
+ }
855
+ const destinationTransactionHashes = (() => {
856
+ if (correlatedTransactionHashes.length > 0) return correlatedTransactionHashes
857
+ if (uncorrelatedTransactionHashes.length === 1) return uncorrelatedTransactionHashes
858
+ return []
859
+ })()
860
+ const providerState = { relay: sourceObservation }
861
+ if (destinationTransactionHashes.length === 0)
862
+ return {
863
+ providerState,
864
+ providerTransactionHashes: [sourceObservation.sourceSweepTransactionHash],
865
+ type: 'pending',
866
+ }
867
+
868
+ return {
869
+ destinationTransactionHashes,
870
+ providerState: {
871
+ relay: {
872
+ ...sourceObservation,
873
+ destinationObservedAt: options.now().toISOString(),
874
+ },
875
+ },
876
+ providerTransactionHashes: [
877
+ sourceObservation.sourceSweepTransactionHash,
878
+ ...destinationTransactionHashes,
879
+ ],
880
+ type: 'verified',
881
+ }
882
+ }
883
+ }
884
+
885
+ function chainsEndpoint(options: Pick<DepositAddressObserverOptions, 'baseUrl'>): URL {
886
+ const endpoint = new URL(options.baseUrl ?? defaultBaseUrl)
887
+ endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/chains`
888
+ return endpoint
889
+ }
890
+
891
+ function decodeRelayErc20Deposit(data: Hex.Hex) {
892
+ const fullAllowanceLength = 2 + 2 * (4 + 32 * 3)
893
+ const withAmountLength = 2 + 2 * (4 + 32 * 4)
894
+ try {
895
+ if (data.length === withAmountLength) {
896
+ const [depositor, token, amount, id] = AbiFunction.decodeData(depositErc20WithAmount, data)
897
+ return { amount, depositor, id, token }
898
+ }
899
+ if (data.length === fullAllowanceLength) {
900
+ const [depositor, token, id] = AbiFunction.decodeData(depositErc20FullAllowance, data)
901
+ return { depositor, id, token }
902
+ }
903
+ return undefined
904
+ } catch {
905
+ return undefined
906
+ }
907
+ }
908
+
909
+ function decodeRelayTransfer(data: Hex.Hex) {
910
+ const encodedLength = 2 + 2 * (4 + 32 * 2)
911
+ if (data.length !== encodedLength && data.length !== encodedLength + 64) return undefined
912
+ try {
913
+ const [recipient, amount] = AbiFunction.decodeData(
914
+ relayTransfer,
915
+ data.slice(0, encodedLength) as Hex.Hex,
916
+ )
917
+ return {
918
+ amount,
919
+ ...(data.length > encodedLength
920
+ ? { orderId: `0x${data.slice(encodedLength)}` as Hex.Hex }
921
+ : {}),
922
+ recipient,
923
+ }
924
+ } catch {
925
+ return undefined
926
+ }
927
+ }
928
+
929
+ function decodeEvent<const event extends AbiEvent.AbiEvent>(
930
+ abiEvent: event,
931
+ log: { data: Hex.Hex; topics: readonly Hex.Hex[] },
932
+ ): AbiEvent.decode.ReturnType<event> | undefined {
933
+ try {
934
+ return AbiEvent.decode(abiEvent, log)
935
+ } catch {
936
+ return undefined
937
+ }
938
+ }
939
+
940
+ function jsonRpcClient(options: { fetch: FundingProvider.Fetch; urls: readonly string[] }) {
941
+ return createClient({
942
+ transport: fallback(
943
+ options.urls.map((url) => {
944
+ const transport = RpcTransport.fromHttp(url, { fetchFn: options.fetch })
945
+ return custom(transport, { retryCount: 0 })
946
+ }),
947
+ ),
948
+ })
949
+ }
950
+
951
+ function minimum(left: bigint, right: bigint): bigint {
952
+ return left < right ? left : right
953
+ }
954
+
955
+ function relayEvmChain(
956
+ chains: readonly RelayChain[],
957
+ chain: FundingProvider.ChainRef,
958
+ ): RelayChain | undefined {
959
+ if (chain.kind !== 'evm') return undefined
960
+ const match = /^eip155:(\d+)$/.exec(chain.id)
961
+ if (!match?.[1]) return undefined
962
+ return chains.find((candidate) => candidate.id === Number(match[1]))
963
+ }
964
+
478
965
  type VerifyWebhookOptions = {
479
966
  body: string
480
967
  secret: string
@@ -485,8 +972,7 @@ type VerifyWebhookOptions = {
485
972
  function verifyWebhook(options: VerifyWebhookOptions): FundingProvider.Webhook.verify.ReturnType {
486
973
  const timestamp = Number(options.timestamp)
487
974
  if (!Number.isSafeInteger(timestamp)) throw new WebhookSignatureError()
488
- const now = Math.floor(Date.now() / 1_000)
489
- if (Math.abs(now - timestamp) > webhookSignatureToleranceSeconds)
975
+ if (Math.abs(Date.now() - timestamp) > webhookSignatureToleranceMs)
490
976
  throw new WebhookSignatureError()
491
977
  if (!/^[0-9a-fA-F]{64}$/.test(options.signature)) throw new WebhookSignatureError()
492
978
  const expected = Hash.hmac256(
@@ -506,7 +992,10 @@ function verifyWebhook(options: VerifyWebhookOptions): FundingProvider.Webhook.v
506
992
  if (!parsed.success) throw new WebhookPayloadError()
507
993
  return {
508
994
  address: parsed.data.data.depositAddress?.address,
995
+ providerStatus: webhookStatus(parsed.data.data.status),
996
+ providerUpdatedAt: webhookTimestamp(parsed.data.data.updatedAt),
509
997
  requestId: parsed.data.data.requestId,
998
+ sentAt: webhookTimestamp(timestamp),
510
999
  }
511
1000
  }
512
1001
 
@@ -528,7 +1017,7 @@ function quoteEndpoint(options: relay.Options): URL {
528
1017
 
529
1018
  function requestsEndpoint(options: relay.Options): URL {
530
1019
  const endpoint = new URL(options.baseUrl ?? defaultBaseUrl)
531
- endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/requests/v2`
1020
+ endpoint.pathname = `${endpoint.pathname.replace(/\/+$/, '')}/requests/v3`
532
1021
  return endpoint
533
1022
  }
534
1023
 
@@ -540,6 +1029,18 @@ function timingSafeEqual(left: string, right: string): boolean {
540
1029
  return mismatch === 0
541
1030
  }
542
1031
 
1032
+ function webhookStatus(status: string): string {
1033
+ return (webhookStatuses as readonly string[]).includes(status) ? status : 'unknown'
1034
+ }
1035
+
1036
+ function webhookTimestamp(value: number): string {
1037
+ try {
1038
+ return new Date(value).toISOString()
1039
+ } catch {
1040
+ throw new WebhookPayloadError()
1041
+ }
1042
+ }
1043
+
543
1044
  function relayChainId(chain: FundingProvider.Chain): number {
544
1045
  if (chain.id.startsWith('eip155:')) return Number(FundingChain.eip155Id(chain))
545
1046
  // Relay uses a numeric chain ID for Solana instead of its CAIP-2 reference.