tapimo 0.11.0 → 0.12.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 (339) hide show
  1. package/dist/App.d.ts +2296 -1010
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +2 -2
  4. package/dist/App.js.map +1 -1
  5. package/dist/Assets.d.ts +15 -0
  6. package/dist/Assets.d.ts.map +1 -1
  7. package/dist/Assets.js +7 -0
  8. package/dist/Assets.js.map +1 -1
  9. package/dist/admin/App.d.ts +53 -4
  10. package/dist/admin/App.d.ts.map +1 -1
  11. package/dist/admin/apps/api-keys.d.ts +4 -4
  12. package/dist/admin/apps/earn-vaults.d.ts +114 -0
  13. package/dist/admin/apps/earn-vaults.d.ts.map +1 -1
  14. package/dist/admin/apps/earn-vaults.js +59 -6
  15. package/dist/admin/apps/earn-vaults.js.map +1 -1
  16. package/dist/admin/apps/organizations.d.ts.map +1 -1
  17. package/dist/admin/apps/organizations.js +4 -2
  18. package/dist/admin/apps/organizations.js.map +1 -1
  19. package/dist/admin/apps/verified-tokens.d.ts +9 -9
  20. package/dist/admin/ui.generated.d.ts +1 -1
  21. package/dist/admin/ui.generated.d.ts.map +1 -1
  22. package/dist/admin/ui.generated.js +151 -151
  23. package/dist/admin/ui.generated.js.map +1 -1
  24. package/dist/apps/data/App.d.ts +1699 -749
  25. package/dist/apps/data/App.d.ts.map +1 -1
  26. package/dist/apps/data/App.js +56 -7
  27. package/dist/apps/data/App.js.map +1 -1
  28. package/dist/apps/data/ZoneSelection.d.ts +62 -0
  29. package/dist/apps/data/ZoneSelection.d.ts.map +1 -0
  30. package/dist/apps/data/ZoneSelection.js +44 -0
  31. package/dist/apps/data/ZoneSelection.js.map +1 -0
  32. package/dist/apps/data/routes/activities.d.ts +2367 -4
  33. package/dist/apps/data/routes/activities.d.ts.map +1 -1
  34. package/dist/apps/data/routes/activities.js +368 -87
  35. package/dist/apps/data/routes/activities.js.map +1 -1
  36. package/dist/apps/data/routes/addresses.d.ts +471 -340
  37. package/dist/apps/data/routes/addresses.d.ts.map +1 -1
  38. package/dist/apps/data/routes/balances.d.ts +28 -28
  39. package/dist/apps/data/routes/balances.js +1 -1
  40. package/dist/apps/data/routes/balances.js.map +1 -1
  41. package/dist/apps/data/routes/blocks.d.ts +4 -4
  42. package/dist/apps/data/routes/blocks.js +1 -1
  43. package/dist/apps/data/routes/blocks.js.map +1 -1
  44. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  45. package/dist/apps/data/routes/earn.d.ts +131 -6
  46. package/dist/apps/data/routes/earn.d.ts.map +1 -1
  47. package/dist/apps/data/routes/earn.js +157 -25
  48. package/dist/apps/data/routes/earn.js.map +1 -1
  49. package/dist/apps/data/routes/exchanges.d.ts +825 -119
  50. package/dist/apps/data/routes/exchanges.d.ts.map +1 -1
  51. package/dist/apps/data/routes/exchanges.js +16 -13
  52. package/dist/apps/data/routes/exchanges.js.map +1 -1
  53. package/dist/apps/data/routes/fee-amm.d.ts.map +1 -1
  54. package/dist/apps/data/routes/fee-amm.js +9 -11
  55. package/dist/apps/data/routes/fee-amm.js.map +1 -1
  56. package/dist/apps/data/routes/indexer.d.ts +34 -34
  57. package/dist/apps/data/routes/receipts.d.ts +230 -230
  58. package/dist/apps/data/routes/receipts.js +1 -1
  59. package/dist/apps/data/routes/receipts.js.map +1 -1
  60. package/dist/apps/data/routes/rpc.js +24 -10
  61. package/dist/apps/data/routes/rpc.js.map +1 -1
  62. package/dist/apps/data/routes/tokenlist.d.ts.map +1 -1
  63. package/dist/apps/data/routes/tokenlist.js +16 -15
  64. package/dist/apps/data/routes/tokenlist.js.map +1 -1
  65. package/dist/apps/data/routes/tokens.d.ts +14 -0
  66. package/dist/apps/data/routes/tokens.d.ts.map +1 -1
  67. package/dist/apps/data/routes/tokens.js +34 -15
  68. package/dist/apps/data/routes/tokens.js.map +1 -1
  69. package/dist/apps/data/routes/transactions.js +1 -1
  70. package/dist/apps/data/routes/transactions.js.map +1 -1
  71. package/dist/apps/data/routes/transfers.d.ts.map +1 -1
  72. package/dist/apps/data/routes/transfers.js +7 -9
  73. package/dist/apps/data/routes/transfers.js.map +1 -1
  74. package/dist/apps/data/routes/webhooks.d.ts +4 -4
  75. package/dist/apps/data/routes/webhooks.d.ts.map +1 -1
  76. package/dist/apps/data/routes/webhooks.js +2 -2
  77. package/dist/apps/data/routes/webhooks.js.map +1 -1
  78. package/dist/apps/data/routes/zones.d.ts +30 -30
  79. package/dist/apps/funding/App.d.ts +27 -27
  80. package/dist/apps/funding/routes/chains.d.ts.map +1 -1
  81. package/dist/apps/funding/routes/chains.js +5 -4
  82. package/dist/apps/funding/routes/chains.js.map +1 -1
  83. package/dist/apps/funding/routes/deposit-addresses.d.ts +16 -16
  84. package/dist/apps/funding/routes/deposit-addresses.d.ts.map +1 -1
  85. package/dist/apps/funding/routes/deposit-addresses.js +8 -8
  86. package/dist/apps/funding/routes/deposit-addresses.js.map +1 -1
  87. package/dist/apps/funding/routes/providers.d.ts +13 -13
  88. package/dist/apps/funding/routes/providers.d.ts.map +1 -1
  89. package/dist/apps/funding/routes/providers.js +3 -2
  90. package/dist/apps/funding/routes/providers.js.map +1 -1
  91. package/dist/apps/funding/routes/quotes.d.ts.map +1 -1
  92. package/dist/apps/funding/routes/quotes.js +2 -2
  93. package/dist/apps/funding/routes/quotes.js.map +1 -1
  94. package/dist/apps/management/App.d.ts +627 -293
  95. package/dist/apps/management/App.d.ts.map +1 -1
  96. package/dist/apps/management/Billing.d.ts.map +1 -1
  97. package/dist/apps/management/Billing.js +19 -6
  98. package/dist/apps/management/Billing.js.map +1 -1
  99. package/dist/apps/management/routes/api-keys.d.ts +114 -54
  100. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  101. package/dist/apps/management/routes/api-keys.js +76 -30
  102. package/dist/apps/management/routes/api-keys.js.map +1 -1
  103. package/dist/apps/management/routes/billing.d.ts +103 -30
  104. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  105. package/dist/apps/management/routes/billing.js +91 -32
  106. package/dist/apps/management/routes/billing.js.map +1 -1
  107. package/dist/apps/management/routes/faucet.d.ts +33 -33
  108. package/dist/apps/management/routes/faucet.d.ts.map +1 -1
  109. package/dist/apps/management/routes/faucet.js +23 -12
  110. package/dist/apps/management/routes/faucet.js.map +1 -1
  111. package/dist/apps/management/routes/invitations.d.ts +46 -13
  112. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  113. package/dist/apps/management/routes/invitations.js +79 -29
  114. package/dist/apps/management/routes/invitations.js.map +1 -1
  115. package/dist/apps/management/routes/invite-links.d.ts +86 -41
  116. package/dist/apps/management/routes/invite-links.d.ts.map +1 -1
  117. package/dist/apps/management/routes/invite-links.js +100 -41
  118. package/dist/apps/management/routes/invite-links.js.map +1 -1
  119. package/dist/apps/management/routes/me.d.ts +5 -5
  120. package/dist/apps/management/routes/me.d.ts.map +1 -1
  121. package/dist/apps/management/routes/me.js +14 -10
  122. package/dist/apps/management/routes/me.js.map +1 -1
  123. package/dist/apps/management/routes/members.d.ts +93 -72
  124. package/dist/apps/management/routes/members.d.ts.map +1 -1
  125. package/dist/apps/management/routes/members.js +37 -18
  126. package/dist/apps/management/routes/members.js.map +1 -1
  127. package/dist/apps/management/routes/orgs.d.ts +48 -8
  128. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  129. package/dist/apps/management/routes/orgs.js +48 -19
  130. package/dist/apps/management/routes/orgs.js.map +1 -1
  131. package/dist/apps/management/routes/projects.d.ts +90 -28
  132. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  133. package/dist/apps/management/routes/projects.js +72 -21
  134. package/dist/apps/management/routes/projects.js.map +1 -1
  135. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  136. package/dist/apps/management/routes/scopes.js +8 -5
  137. package/dist/apps/management/routes/scopes.js.map +1 -1
  138. package/dist/apps/management/routes/usage.d.ts +78 -78
  139. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  140. package/dist/apps/management/routes/usage.js +41 -31
  141. package/dist/apps/management/routes/usage.js.map +1 -1
  142. package/dist/apps/management/routes/verified-token-requests.d.ts +34 -6
  143. package/dist/apps/management/routes/verified-token-requests.d.ts.map +1 -1
  144. package/dist/apps/management/routes/verified-token-requests.js +36 -17
  145. package/dist/apps/management/routes/verified-token-requests.js.map +1 -1
  146. package/dist/apps/management/routes/webhooks.d.ts +147 -130
  147. package/dist/apps/management/routes/webhooks.d.ts.map +1 -1
  148. package/dist/apps/management/routes/webhooks.js +80 -35
  149. package/dist/apps/management/routes/webhooks.js.map +1 -1
  150. package/dist/apps/mpp/internal/Adapter.d.ts +1 -1
  151. package/dist/apps/mpp/internal/Adapter.d.ts.map +1 -1
  152. package/dist/apps/mpp/internal/Adapter.js +12 -0
  153. package/dist/apps/mpp/internal/Adapter.js.map +1 -1
  154. package/dist/db/Db.d.ts +2 -0
  155. package/dist/db/Db.d.ts.map +1 -1
  156. package/dist/db/Db.js +40 -0
  157. package/dist/db/Db.js.map +1 -1
  158. package/dist/db/Schema.d.ts +23 -1
  159. package/dist/db/Schema.d.ts.map +1 -1
  160. package/dist/db/Schema.js +19 -3
  161. package/dist/db/Schema.js.map +1 -1
  162. package/dist/db/tables/earnVaults.d.ts +5 -1
  163. package/dist/db/tables/earnVaults.d.ts.map +1 -1
  164. package/dist/db/tables/earnVaults.js +5 -0
  165. package/dist/db/tables/earnVaults.js.map +1 -1
  166. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  167. package/dist/db/tables/webhookDeliveries.js +10 -0
  168. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  169. package/dist/db/tables/webhookQueueCompletions.d.ts +15 -0
  170. package/dist/db/tables/webhookQueueCompletions.d.ts.map +1 -0
  171. package/dist/db/tables/webhookQueueCompletions.js +32 -0
  172. package/dist/db/tables/webhookQueueCompletions.js.map +1 -0
  173. package/dist/db/tables/webhookQueueEvents.d.ts +14 -20
  174. package/dist/db/tables/webhookQueueEvents.d.ts.map +1 -1
  175. package/dist/db/tables/webhookQueueEvents.js +68 -47
  176. package/dist/db/tables/webhookQueueEvents.js.map +1 -1
  177. package/dist/db/tables/webhookSubscriptions.d.ts +16 -5
  178. package/dist/db/tables/webhookSubscriptions.d.ts.map +1 -1
  179. package/dist/db/tables/webhookSubscriptions.js +29 -5
  180. package/dist/db/tables/webhookSubscriptions.js.map +1 -1
  181. package/dist/internal/Auth.d.ts +8 -1
  182. package/dist/internal/Auth.d.ts.map +1 -1
  183. package/dist/internal/Auth.js +13 -3
  184. package/dist/internal/Auth.js.map +1 -1
  185. package/dist/internal/Cache.d.ts.map +1 -1
  186. package/dist/internal/Cache.js +11 -7
  187. package/dist/internal/Cache.js.map +1 -1
  188. package/dist/internal/EarnVaults.d.ts +41 -2
  189. package/dist/internal/EarnVaults.d.ts.map +1 -1
  190. package/dist/internal/EarnVaults.js +167 -0
  191. package/dist/internal/EarnVaults.js.map +1 -1
  192. package/dist/internal/OpenApi.d.ts +26 -22
  193. package/dist/internal/OpenApi.d.ts.map +1 -1
  194. package/dist/internal/OpenApi.js +128 -39
  195. package/dist/internal/OpenApi.js.map +1 -1
  196. package/dist/internal/Response.d.ts +13 -8
  197. package/dist/internal/Response.d.ts.map +1 -1
  198. package/dist/internal/Response.js +16 -5
  199. package/dist/internal/Response.js.map +1 -1
  200. package/dist/internal/Schema.d.ts +5 -7
  201. package/dist/internal/Schema.d.ts.map +1 -1
  202. package/dist/internal/Schema.js +6 -7
  203. package/dist/internal/Schema.js.map +1 -1
  204. package/dist/internal/Store.d.ts +15 -6
  205. package/dist/internal/Store.d.ts.map +1 -1
  206. package/dist/internal/Store.js +45 -15
  207. package/dist/internal/Store.js.map +1 -1
  208. package/dist/internal/Tidx.d.ts +2 -0
  209. package/dist/internal/Tidx.d.ts.map +1 -1
  210. package/dist/internal/Tidx.js +16 -4
  211. package/dist/internal/Tidx.js.map +1 -1
  212. package/dist/internal/Webhooks.d.ts +29 -12
  213. package/dist/internal/Webhooks.d.ts.map +1 -1
  214. package/dist/internal/Webhooks.js +37 -20
  215. package/dist/internal/Webhooks.js.map +1 -1
  216. package/dist/internal/funding/Deposit.d.ts +2 -2
  217. package/dist/internal/funding/Deposit.d.ts.map +1 -1
  218. package/dist/internal/funding/Deposit.js +9 -8
  219. package/dist/internal/funding/Deposit.js.map +1 -1
  220. package/dist/internal/funding/DepositAddress.d.ts +4 -4
  221. package/dist/internal/funding/DepositAddress.d.ts.map +1 -1
  222. package/dist/internal/funding/DepositAddress.js +9 -8
  223. package/dist/internal/funding/DepositAddress.js.map +1 -1
  224. package/dist/internal/funding/Provider.d.ts.map +1 -1
  225. package/dist/internal/funding/Provider.js +13 -12
  226. package/dist/internal/funding/Provider.js.map +1 -1
  227. package/dist/internal/funding/providers/stargate.d.ts +19 -0
  228. package/dist/internal/funding/providers/stargate.d.ts.map +1 -0
  229. package/dist/internal/funding/providers/stargate.js +143 -0
  230. package/dist/internal/funding/providers/stargate.js.map +1 -0
  231. package/dist/providers/index.d.ts +7 -0
  232. package/dist/providers/index.d.ts.map +1 -1
  233. package/dist/providers/index.js +3 -0
  234. package/dist/providers/index.js.map +1 -1
  235. package/package.json +2 -2
  236. package/src/App.test.ts +48 -0
  237. package/src/App.ts +4 -2
  238. package/src/Assets.test.ts +18 -0
  239. package/src/Assets.ts +22 -0
  240. package/src/Client.test-d.ts +265 -16
  241. package/src/admin/apps/earn-vaults.test.ts +143 -0
  242. package/src/admin/apps/earn-vaults.ts +83 -6
  243. package/src/admin/apps/organizations.ts +6 -2
  244. package/src/admin/ui/src/routes/api-keys.tsx +36 -0
  245. package/src/admin/ui.generated.ts +151 -189
  246. package/src/apps/data/App.test.ts +31 -0
  247. package/src/apps/data/App.ts +56 -8
  248. package/src/apps/data/ZoneSelection.test.ts +196 -0
  249. package/src/apps/data/ZoneSelection.ts +108 -0
  250. package/src/apps/data/routes/activities.test.ts +328 -1
  251. package/src/apps/data/routes/activities.ts +498 -93
  252. package/src/apps/data/routes/balances.ts +1 -1
  253. package/src/apps/data/routes/blocks.ts +1 -1
  254. package/src/apps/data/routes/earn.test.ts +183 -23
  255. package/src/apps/data/routes/earn.ts +225 -29
  256. package/src/apps/data/routes/earn.zone-access.test.ts +46 -0
  257. package/src/apps/data/routes/exchanges.test.ts +31 -1
  258. package/src/apps/data/routes/exchanges.ts +19 -15
  259. package/src/apps/data/routes/fee-amm.ts +18 -11
  260. package/src/apps/data/routes/receipts.ts +1 -1
  261. package/src/apps/data/routes/rpc.test.ts +75 -0
  262. package/src/apps/data/routes/rpc.ts +23 -9
  263. package/src/apps/data/routes/tokenlist.ts +21 -18
  264. package/src/apps/data/routes/tokens.test.ts +29 -0
  265. package/src/apps/data/routes/tokens.ts +38 -15
  266. package/src/apps/data/routes/transactions.ts +1 -1
  267. package/src/apps/data/routes/transfers.test.ts +3 -3
  268. package/src/apps/data/routes/transfers.ts +12 -9
  269. package/src/apps/data/routes/webhooks.ts +6 -2
  270. package/src/apps/data/routes/zones.test.ts +19 -13
  271. package/src/apps/funding/routes/chains.test.ts +211 -7
  272. package/src/apps/funding/routes/chains.ts +19 -12
  273. package/src/apps/funding/routes/deposit-addresses.test.ts +394 -0
  274. package/src/apps/funding/routes/deposit-addresses.ts +84 -72
  275. package/src/apps/funding/routes/providers.ts +9 -5
  276. package/src/apps/funding/routes/quotes.test.ts +46 -0
  277. package/src/apps/funding/routes/quotes.ts +8 -5
  278. package/src/apps/funding/routes/transfers.test.ts +35 -32
  279. package/src/apps/management/Billing.test.ts +26 -0
  280. package/src/apps/management/Billing.ts +24 -7
  281. package/src/apps/management/routes/api-keys.test.ts +178 -0
  282. package/src/apps/management/routes/api-keys.ts +203 -144
  283. package/src/apps/management/routes/billing.test.ts +209 -0
  284. package/src/apps/management/routes/billing.ts +254 -177
  285. package/src/apps/management/routes/faucet.test.ts +39 -0
  286. package/src/apps/management/routes/faucet.ts +75 -41
  287. package/src/apps/management/routes/invitations.test.ts +216 -0
  288. package/src/apps/management/routes/invitations.ts +186 -109
  289. package/src/apps/management/routes/invite-links.test.ts +263 -1
  290. package/src/apps/management/routes/invite-links.ts +295 -214
  291. package/src/apps/management/routes/me.test.ts +49 -0
  292. package/src/apps/management/routes/me.ts +42 -32
  293. package/src/apps/management/routes/members.test.ts +124 -1
  294. package/src/apps/management/routes/members.ts +81 -51
  295. package/src/apps/management/routes/orgs.test.ts +176 -0
  296. package/src/apps/management/routes/orgs.ts +101 -59
  297. package/src/apps/management/routes/projects.test.ts +183 -0
  298. package/src/apps/management/routes/projects.ts +124 -60
  299. package/src/apps/management/routes/scopes.test.ts +44 -0
  300. package/src/apps/management/routes/scopes.ts +37 -28
  301. package/src/apps/management/routes/usage.test.ts +92 -0
  302. package/src/apps/management/routes/usage.ts +186 -151
  303. package/src/apps/management/routes/verified-token-requests.test.ts +114 -0
  304. package/src/apps/management/routes/verified-token-requests.ts +166 -133
  305. package/src/apps/management/routes/webhooks.test.ts +234 -0
  306. package/src/apps/management/routes/webhooks.ts +191 -78
  307. package/src/apps/mpp/internal/Adapter.ts +13 -1
  308. package/src/db/Db.test.ts +41 -0
  309. package/src/db/Db.ts +62 -0
  310. package/src/db/Schema.ts +28 -3
  311. package/src/db/tables/earnVaults.test.ts +22 -0
  312. package/src/db/tables/earnVaults.ts +11 -1
  313. package/src/db/tables/webhookDeliveries.ts +13 -0
  314. package/src/db/tables/webhookQueueCompletions.ts +55 -0
  315. package/src/db/tables/webhookQueueEvents.ts +95 -61
  316. package/src/db/tables/webhookSubscriptions.ts +45 -6
  317. package/src/internal/Auth.ts +23 -5
  318. package/src/internal/Cache.ts +14 -7
  319. package/src/internal/EarnVaults.capabilities.test.ts +33 -0
  320. package/src/internal/EarnVaults.test.ts +79 -0
  321. package/src/internal/EarnVaults.ts +277 -0
  322. package/src/internal/OpenApi.ts +165 -49
  323. package/src/internal/Response.test.ts +95 -0
  324. package/src/internal/Response.ts +40 -2
  325. package/src/internal/Schema.test.ts +16 -0
  326. package/src/internal/Schema.ts +7 -7
  327. package/src/internal/Store.test.ts +186 -0
  328. package/src/internal/Store.ts +57 -17
  329. package/src/internal/Tidx.test.ts +19 -0
  330. package/src/internal/Tidx.ts +16 -4
  331. package/src/internal/Webhooks.test.ts +122 -8
  332. package/src/internal/Webhooks.ts +59 -29
  333. package/src/internal/funding/Deposit.ts +71 -64
  334. package/src/internal/funding/DepositAddress.ts +50 -43
  335. package/src/internal/funding/Provider.ts +164 -142
  336. package/src/internal/funding/providers/index.test.ts +185 -0
  337. package/src/internal/funding/providers/stargate.ts +184 -0
  338. package/src/providers/index.test.ts +1 -0
  339. package/src/providers/index.ts +9 -0
@@ -348,6 +348,41 @@ describe('recordSuccess / recordFailure', () => {
348
348
  expect(read?.lastDeliveryAt).toBe('2026-01-01T00:00:00.000Z')
349
349
  })
350
350
 
351
+ test('recordSuccess refreshes the stamp at most once per precision window', async () => {
352
+ const db = TestApp.database()
353
+ const created = await Webhooks.createSubscription(db, createInput())
354
+ const stamped = await Webhooks.recordSuccess(db, created, { now: fixedNow })
355
+ expect(stamped.lastDeliveryAt).toBe(fixedNow().toISOString())
356
+
357
+ // A success moments later has nothing to move: the counter is already zero
358
+ // and the stamp is inside its window, so the row is never rewritten.
359
+ const soon = new Date(fixedNow().getTime() + 1_000)
360
+ await Webhooks.recordSuccess(db, stamped, { now: () => soon })
361
+ const unmoved = await Webhooks.getSubscription(db, apiKeyOwner, created.id)
362
+ expect(unmoved?.lastDeliveryAt, 'the stored stamp must not move').toBe(fixedNow().toISOString())
363
+
364
+ // Past any plausible window; the exact bound is internal.
365
+ const later = new Date(fixedNow().getTime() + 3_600_000)
366
+ await Webhooks.recordSuccess(db, stamped, { now: () => later })
367
+ const moved = await Webhooks.getSubscription(db, apiKeyOwner, created.id)
368
+ expect(moved?.lastDeliveryAt).toBe(later.toISOString())
369
+ })
370
+
371
+ test('recordSuccess always resets a non-zero failure counter', async () => {
372
+ const db = TestApp.database()
373
+ const created = await Webhooks.createSubscription(db, createInput())
374
+ // Stamp first, so only the counter is out of date: the reset must not be
375
+ // suppressed by a fresh stamp.
376
+ const stamped = await Webhooks.recordSuccess(db, created, { now: fixedNow })
377
+ const failed = await Webhooks.recordFailure(db, stamped, { now: fixedNow })
378
+ expect(failed.failureCount).toBe(1)
379
+
380
+ await Webhooks.recordSuccess(db, failed, { now: fixedNow })
381
+
382
+ const read = await Webhooks.getSubscription(db, apiKeyOwner, created.id)
383
+ expect(read?.failureCount, 'a pending reset must never be skipped').toBe(0)
384
+ })
385
+
351
386
  test('recordFailure auto-disables at the threshold and drops from the active index', async () => {
352
387
  const db = TestApp.database()
353
388
  let subscription = await Webhooks.createSubscription(db, createInput())
@@ -375,6 +410,37 @@ describe('recordSuccess / recordFailure', () => {
375
410
  await Webhooks.listActiveForChain(db, { chainId: 4217, eventTypes: ['token:transfer'] }),
376
411
  ).toMatchInlineSnapshot(`[]`)
377
412
  })
413
+
414
+ test('recordFailure leaves an already-disabled subscription alone at the cap', async () => {
415
+ const db = TestApp.database()
416
+ let subscription = await Webhooks.createSubscription(db, createInput())
417
+ subscription = await Webhooks.recordFailure(db, subscription, { maxFailures: 2, now: fixedNow })
418
+ subscription = await Webhooks.recordFailure(db, subscription, { maxFailures: 2, now: fixedNow })
419
+ expect(subscription.status).toBe('disabled')
420
+
421
+ // A straggler failing after the disable has nothing to move: the row is
422
+ // never rewritten.
423
+ const later = new Date(fixedNow().getTime() + 1_000)
424
+ await Webhooks.recordFailure(db, subscription, { maxFailures: 2, now: () => later })
425
+
426
+ const read = await Webhooks.getSubscription(db, apiKeyOwner, subscription.id)
427
+ expect(read?.failureCount, 'the stored counter must not move').toBe(2)
428
+ expect(read?.updatedAt, 'the stored stamp must not move').toBe(fixedNow().toISOString())
429
+ })
430
+
431
+ test('recordFailure re-disables a re-enabled subscription still at the cap', async () => {
432
+ const db = TestApp.database()
433
+ let subscription = await Webhooks.createSubscription(db, createInput())
434
+ subscription = await Webhooks.recordFailure(db, subscription, { maxFailures: 1, now: fixedNow })
435
+ expect(subscription.status).toBe('disabled')
436
+
437
+ await Webhooks.updateSubscription(db, apiKeyOwner, subscription.id, { status: 'active' })
438
+
439
+ // The counter still sits at the cap, so this failure must write again.
440
+ await Webhooks.recordFailure(db, subscription, { maxFailures: 1, now: fixedNow })
441
+ const read = await Webhooks.getSubscription(db, apiKeyOwner, subscription.id)
442
+ expect(read?.status).toBe('disabled')
443
+ })
378
444
  })
379
445
 
380
446
  describe('sign / verify', () => {
@@ -1015,7 +1081,7 @@ describe('ensureQueueEvents', () => {
1015
1081
  expect(await Webhooks.getQueueEvent(db, reference)).toBeNull()
1016
1082
  })
1017
1083
 
1018
- test('replaces an expired terminal row so replays stay deliverable', async () => {
1084
+ test('stages a replay once its completion has expired', async () => {
1019
1085
  vi.useFakeTimers({ toFake: ['Date'] })
1020
1086
  vi.setSystemTime(fixedNow())
1021
1087
  try {
@@ -1031,9 +1097,9 @@ describe('ensureQueueEvents', () => {
1031
1097
  }),
1032
1098
  ).toBe(true)
1033
1099
 
1034
- // Past the dedupe window an unpruned terminal row must not suppress a
1100
+ // Past the dedupe window an unpruned completion must not suppress a
1035
1101
  // replay.
1036
- vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
1102
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.dedupeRetentionMs + 1_000))
1037
1103
  const replay = await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }])
1038
1104
  expect(replay.created).toBe(1)
1039
1105
  await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
@@ -1044,7 +1110,7 @@ describe('ensureQueueEvents', () => {
1044
1110
  }
1045
1111
  })
1046
1112
 
1047
- test('refreshes the observation stamp when a replay replaces an expired row', async () => {
1113
+ test('stamps a fresh observation on a post-expiry replay', async () => {
1048
1114
  vi.useFakeTimers({ toFake: ['Date'] })
1049
1115
  vi.setSystemTime(fixedNow())
1050
1116
  try {
@@ -1057,7 +1123,7 @@ describe('ensureQueueEvents', () => {
1057
1123
  status: 'succeeded',
1058
1124
  })
1059
1125
 
1060
- vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
1126
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.dedupeRetentionMs + 1_000))
1061
1127
  const observedAt = Date.now() - 250
1062
1128
  await Webhooks.ensureQueueEvents(db, [{ envelope, subscription }], { observedAt })
1063
1129
 
@@ -1070,6 +1136,38 @@ describe('ensureQueueEvents', () => {
1070
1136
  vi.useRealTimers()
1071
1137
  }
1072
1138
  })
1139
+
1140
+ test('suppresses only the completed obligation when staging a mixed batch', async () => {
1141
+ const db = TestApp.database()
1142
+ const done = await stagedObligation(db, 4)
1143
+ const claim = await Webhooks.claimQueueEvent(db, done.reference)
1144
+ if (claim.type !== 'claimed') throw new Error(`Unexpected claim: ${claim.type}`)
1145
+ await Webhooks.completeQueueEvent(db, done.reference, {
1146
+ claimedAt: claim.record.attemptingAt!,
1147
+ status: 'failed',
1148
+ })
1149
+
1150
+ const fresh = Webhooks.buildEnvelope({
1151
+ blockNumber: 124,
1152
+ createdAt: new Date(),
1153
+ data: { amount: '1' },
1154
+ logIndex: 5,
1155
+ subscription: done.subscription,
1156
+ })
1157
+ const batch = await Webhooks.ensureQueueEvents(db, [
1158
+ { envelope: done.envelope, subscription: done.subscription },
1159
+ { envelope: fresh, subscription: done.subscription },
1160
+ ])
1161
+
1162
+ expect(batch.created).toBe(1)
1163
+ await expect(Webhooks.claimQueueEvent(db, done.reference)).resolves.toEqual({
1164
+ status: 'failed',
1165
+ type: 'terminal',
1166
+ })
1167
+ await expect(Webhooks.claimQueueEvent(db, batch.references[1]!)).resolves.toMatchObject({
1168
+ type: 'claimed',
1169
+ })
1170
+ })
1073
1171
  })
1074
1172
 
1075
1173
  describe('claimQueueEvent', () => {
@@ -1082,6 +1180,22 @@ describe('claimQueueEvent', () => {
1082
1180
  vi.useRealTimers()
1083
1181
  })
1084
1182
 
1183
+ test('reads an expired completion as missing', async () => {
1184
+ const db = TestApp.database()
1185
+ const { reference } = await stagedObligation(db)
1186
+ const claim = await Webhooks.claimQueueEvent(db, reference)
1187
+ if (claim.type !== 'claimed') throw new Error(`Unexpected claim: ${claim.type}`)
1188
+ await Webhooks.completeQueueEvent(db, reference, {
1189
+ claimedAt: claim.record.attemptingAt!,
1190
+ status: 'succeeded',
1191
+ })
1192
+
1193
+ // Matches a pruned terminal row: past the dedupe window the completion is
1194
+ // invisible everywhere, not only to staging.
1195
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.dedupeRetentionMs + 1_000))
1196
+ await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toEqual({ type: 'missing' })
1197
+ })
1198
+
1085
1199
  test('claims a due obligation once and reclaims after staleness', async () => {
1086
1200
  const db = TestApp.database()
1087
1201
  const { reference } = await stagedObligation(db)
@@ -1146,17 +1260,17 @@ describe('claimQueueEvent', () => {
1146
1260
 
1147
1261
  // Pending obligations never expire; only terminal transitions stamp a
1148
1262
  // dedupe deadline.
1149
- vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs * 3))
1263
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.dedupeRetentionMs * 3))
1150
1264
  await expect(Webhooks.claimQueueEvent(db, reference)).resolves.toMatchObject({
1151
1265
  type: 'claimed',
1152
1266
  })
1153
1267
  })
1154
1268
 
1155
- test('never prunes pending work', async () => {
1269
+ test('leaves pending obligations claimable across a prune', async () => {
1156
1270
  const db = TestApp.database()
1157
1271
  const { reference } = await stagedObligation(db)
1158
1272
 
1159
- vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.deliveryRetentionMs + 1_000))
1273
+ vi.setSystemTime(new Date(fixedNow().getTime() + Webhooks.dedupeRetentionMs + 1_000))
1160
1274
  await Webhooks.pruneExpired(db)
1161
1275
  const claim = await Webhooks.claimQueueEvent(db, reference)
1162
1276
  expect(claim.type).toBe('claimed')
@@ -3,6 +3,7 @@ import { Hash, Hex } from 'ox'
3
3
  import type * as app_Webhooks from '../apps/data/routes/webhooks.js'
4
4
  import type * as Db from '../db/Db.js'
5
5
  import * as WebhookDeliveries from '../db/tables/webhookDeliveries.js'
6
+ import * as WebhookQueueCompletions from '../db/tables/webhookQueueCompletions.js'
6
7
  import * as WebhookQueueEvents from '../db/tables/webhookQueueEvents.js'
7
8
  import * as WebhookSubscriptions from '../db/tables/webhookSubscriptions.js'
8
9
  import type * as Metrics from '../Metrics.js'
@@ -95,7 +96,8 @@ export type Subscription = {
95
96
  filters: Record<string, unknown>
96
97
  /** Subscription id (`wh_…`). */
97
98
  id: string
98
- /** ISO timestamp of the last successful delivery, if any. */
99
+ /** ISO timestamp of the last successful delivery, if any. Display only:
100
+ * refreshed at most once per precision window. */
99
101
  lastDeliveryAt?: string | undefined
100
102
  /** Authenticated owner. */
101
103
  owner: Owner
@@ -105,7 +107,8 @@ export type Subscription = {
105
107
  secret: string
106
108
  /** Lifecycle status. */
107
109
  status: Status
108
- /** ISO timestamp of the last mutation. */
110
+ /** ISO timestamp of the last mutation. May lag repeat deliveries; see
111
+ * `lastDeliveryAt`. */
109
112
  updatedAt: string
110
113
  }
111
114
 
@@ -406,11 +409,18 @@ export async function getCursor(db: Db.Db, subscriptionId: string): Promise<stri
406
409
  * deliveries past retention. Reads already filter expiry at query time; this
407
410
  * is the janitor reclaiming space, run best-effort on a schedule.
408
411
  */
409
- export async function pruneExpired(db: Db.Db, options: pruneExpired.Options = {}): Promise<void> {
412
+ export async function pruneExpired(
413
+ db: Db.Db,
414
+ options: pruneExpired.Options = {},
415
+ ): Promise<pruneExpired.Result> {
410
416
  const iso = (options.now ?? (() => new Date()))().toISOString()
411
- await WebhookDeliveries.pruneExpired(db, iso)
412
- await WebhookQueueEvents.pruneExpired(db, iso)
413
- await WebhookSubscriptions.pruneExpired(db, iso)
417
+ // Sequential on purpose: the subscription delete cascades into the other
418
+ // tables, so overlapping it with their prunes risks deadlocks.
419
+ return {
420
+ completions: await WebhookQueueCompletions.pruneExpired(db, iso),
421
+ deliveries: await WebhookDeliveries.pruneExpired(db, iso),
422
+ subscriptions: await WebhookSubscriptions.pruneExpired(db, iso),
423
+ }
414
424
  }
415
425
 
416
426
  export declare namespace pruneExpired {
@@ -419,6 +429,16 @@ export declare namespace pruneExpired {
419
429
  /** Clock used for expiry evaluation; injectable for tests. */
420
430
  now?: (() => Date) | undefined
421
431
  }
432
+
433
+ /** Rows deleted per table; a count pinned at the batch bound means expiry is outpacing pruning. */
434
+ type Result = {
435
+ /** Deleted `webhook_queue_completions` rows. */
436
+ completions: number
437
+ /** Deleted `webhook_deliveries` rows. */
438
+ deliveries: number
439
+ /** Deleted `webhook_subscriptions` rows. */
440
+ subscriptions: number
441
+ }
422
442
  }
423
443
 
424
444
  /**
@@ -427,6 +447,13 @@ export declare namespace pruneExpired {
427
447
  */
428
448
  export const deliveryRetentionMs = 7 * 24 * 60 * 60 * 1_000
429
449
 
450
+ /**
451
+ * Completions dedupe window: replays of a completed obligation are suppressed
452
+ * this long. Deliberately its own literal so per-deployment log-retention
453
+ * tuning cannot move it.
454
+ */
455
+ export const dedupeRetentionMs = 7 * 24 * 60 * 60 * 1_000
456
+
430
457
  /** A compact Cloudflare Queue message referencing a durable staged envelope. */
431
458
  export type QueueReference = {
432
459
  /** Idempotent event id of the staged envelope. */
@@ -441,8 +468,6 @@ export type QueueEvent = QueueReference & {
441
468
  createdAt: string
442
469
  /** Full envelope loaded by the Queue consumer. */
443
470
  envelope: Envelope
444
- /** ISO timestamp after which an unconsumed envelope may be pruned. */
445
- expiresAt: string
446
471
  }
447
472
 
448
473
  /** A claim newer than this counts as an in-flight attempt; above the 10s destination timeout default. */
@@ -467,7 +492,6 @@ export async function ensureQueueEvents(
467
492
  options: ensureQueueEvents.Options = {},
468
493
  ): Promise<ensureQueueEvents.Result> {
469
494
  const now = (options.now ?? (() => new Date()))()
470
- const expiresAt = new Date(now.getTime() + (options.ttl ?? deliveryRetentionMs)).toISOString()
471
495
  // Carried so the delivery worker can measure observation-to-attempt without
472
496
  // re-reading the block; replays have no observation and stay null.
473
497
  const observedAt =
@@ -476,9 +500,6 @@ export async function ensureQueueEvents(
476
500
  createdAt: now.toISOString(),
477
501
  envelope,
478
502
  eventId: envelope.id,
479
- // Inert while pending; the meaningful dedupe deadline is stamped at
480
- // terminal transitions.
481
- expiresAt,
482
503
  // Due immediately; the value orders the sweep so it is never null here.
483
504
  nextAttemptAt: now.toISOString(),
484
505
  observedAt,
@@ -499,14 +520,12 @@ export declare namespace ensureQueueEvents {
499
520
  subscription: Subscription
500
521
  }
501
522
 
502
- /** Staged-envelope retention options. */
523
+ /** Staging options. */
503
524
  type Options = {
504
525
  /** Clock used for staging timestamps. */
505
526
  now?: (() => Date) | undefined
506
527
  /** Epoch ms the head owing these events was observed; absent on replays. */
507
528
  observedAt?: number | undefined
508
- /** Staged-envelope TTL in ms; defaults to {@link deliveryRetentionMs}. */
509
- ttl?: number | undefined
510
529
  }
511
530
 
512
531
  /** Created-row count plus references for every requested obligation. */
@@ -542,7 +561,7 @@ export function completeQueueEvent(
542
561
  ): Promise<boolean> {
543
562
  return WebhookQueueEvents.complete(db, reference, {
544
563
  ...options,
545
- expiresAt: new Date(Date.now() + deliveryRetentionMs).toISOString(),
564
+ expiresAt: new Date(Date.now() + dedupeRetentionMs).toISOString(),
546
565
  })
547
566
  }
548
567
 
@@ -552,7 +571,7 @@ export declare namespace completeQueueEvent {
552
571
  /** Claim marker from the claimed record's `attemptingAt`. */
553
572
  claimedAt: string
554
573
  /** Terminal obligation outcome. */
555
- status: WebhookQueueEvents.TerminalStatus
574
+ status: WebhookQueueCompletions.TerminalStatus
556
575
  }
557
576
  }
558
577
 
@@ -766,10 +785,17 @@ export async function getDelivery(
766
785
  */
767
786
  export const maxFailures = 10
768
787
 
788
+ /**
789
+ * How stale `lastDeliveryAt` may get before a success rewrites it. Read only
790
+ * for display, so exactness is traded for not rewriting the row per delivery.
791
+ */
792
+ const lastDeliveryPrecisionMs = 60_000
793
+
769
794
  /**
770
795
  * Records a successful delivery: resets the consecutive-failure counter and
771
- * stamps `lastDeliveryAt`. A column-targeted update, so a concurrent patch is
772
- * never clobbered by the stale copy carried through the delivery queue.
796
+ * refreshes `lastDeliveryAt` at most once per precision window. Returns the
797
+ * written row, or the caller's snapshot when the write was skipped or the row
798
+ * deleted.
773
799
  */
774
800
  export async function recordSuccess(
775
801
  db: Db.Db,
@@ -777,11 +803,13 @@ export async function recordSuccess(
777
803
  options: recordSuccess.Options = {},
778
804
  ): Promise<Subscription> {
779
805
  const now = (options.now ?? (() => new Date()))()
780
- const iso = now.toISOString()
781
- const next = await WebhookSubscriptions.recordSuccess(db, subscription.id, iso)
782
- // A concurrently deleted subscription is not resurrected; return the
783
- // optimistic next state so the caller still observes the outcome.
784
- return next ?? { ...subscription, failureCount: 0, lastDeliveryAt: iso, updatedAt: iso }
806
+ const next = await WebhookSubscriptions.recordSuccess(db, subscription.id, {
807
+ now: now.toISOString(),
808
+ staleBefore: new Date(now.getTime() - lastDeliveryPrecisionMs).toISOString(),
809
+ })
810
+ // No row means the write was skipped or the row was deleted; neither
811
+ // resurrects, so keep the caller's timestamps.
812
+ return next ?? { ...subscription, failureCount: 0 }
785
813
  }
786
814
 
787
815
  export declare namespace recordSuccess {
@@ -796,7 +824,9 @@ export declare namespace recordSuccess {
796
824
  * Records a failed delivery: atomically increments the consecutive-failure
797
825
  * counter and, once it reaches `maxFailures`, sets `status: 'disabled'` so the
798
826
  * subscription drops out of the partial active index (surfaced via the API).
799
- * Returns the updated subscription.
827
+ * Once disabled at the cap the row is left alone, so an outage's in-flight
828
+ * failures stop contending on its lock. Returns the written row, or the
829
+ * caller's snapshot advanced optimistically.
800
830
  */
801
831
  export async function recordFailure(
802
832
  db: Db.Db,
@@ -808,14 +838,14 @@ export async function recordFailure(
808
838
  const max = options.maxFailures ?? maxFailures
809
839
  const next = await WebhookSubscriptions.recordFailure(db, subscription.id, max, iso)
810
840
  if (next) return next
811
- // A concurrently deleted subscription is not resurrected; return the
812
- // optimistic next state so the caller still observes the outcome.
813
- const failureCount = subscription.failureCount + 1
841
+ // No row means skipped (already disabled at the cap) or deleted; neither
842
+ // resurrects, so keep the caller's timestamps. Capped so chained skips
843
+ // cannot grow the counter past what the store would hold.
844
+ const failureCount = Math.min(subscription.failureCount + 1, max)
814
845
  return {
815
846
  ...subscription,
816
847
  failureCount,
817
848
  ...(failureCount >= max ? { status: 'disabled' as const } : {}),
818
- updatedAt: iso,
819
849
  }
820
850
  }
821
851
 
@@ -4,6 +4,7 @@ import type * as Db from '../../db/Db.js'
4
4
  import * as FundingDepositAddresses from '../../db/tables/fundingDepositAddresses.js'
5
5
  import * as FundingDeposits from '../../db/tables/fundingDeposits.js'
6
6
  import * as Id from '../Id.js'
7
+ import * as OpenApi from '../OpenApi.js'
7
8
  import * as Schema from '../Schema.js'
8
9
  import * as Provider from './Provider.js'
9
10
 
@@ -115,22 +116,25 @@ export namespace schema {
115
116
  )
116
117
 
117
118
  /** Customer-safe explanation of a deposit status. */
118
- export const StatusReason = z
119
- .object({
120
- code: z
121
- .enum(statusReasonCodes)
122
- .check(
123
- z.describe('Stable machine-readable reason code.'),
124
- z.meta({ examples: ['delivery_liquidity_unavailable'] }),
125
- ),
126
- message: z
127
- .string()
128
- .check(
129
- z.describe('Human-readable explanation of the current status.'),
130
- z.meta({ examples: ['The required destination amount is not currently available.'] }),
131
- ),
132
- })
133
- .check(z.describe('Why a deposit needs attention or recovery.'))
119
+ export const StatusReason = OpenApi.component(
120
+ z
121
+ .object({
122
+ code: z
123
+ .enum(statusReasonCodes)
124
+ .check(
125
+ z.describe('Stable machine-readable reason code.'),
126
+ z.meta({ examples: ['delivery_liquidity_unavailable'] }),
127
+ ),
128
+ message: z
129
+ .string()
130
+ .check(
131
+ z.describe('Human-readable explanation of the current status.'),
132
+ z.meta({ examples: ['The required destination amount is not currently available.'] }),
133
+ ),
134
+ })
135
+ .check(z.describe('Why a deposit needs attention or recovery.')),
136
+ 'FundingDepositStatusReason',
137
+ )
134
138
 
135
139
  /** Public fields persisted as the deposit snapshot. */
136
140
  export const Snapshot = z
@@ -189,54 +193,57 @@ export namespace schema {
189
193
  )
190
194
 
191
195
  /** One detected funding deposit. */
192
- export const FundingDeposit = z
193
- .object({
194
- createdAt: z
195
- .string()
196
- .check(
197
- z.describe('When the deposit was first detected (ISO 8601).'),
198
- z.meta({ examples: ['2026-08-04T00:01:00.000Z'] }),
199
- ),
200
- depositAddressId: Snapshot.shape.depositAddressId,
201
- detectionTrigger: z.optional(DetectionTrigger),
202
- destinationAmount: Snapshot.shape.destinationAmount,
203
- destinationAmountRequired: Snapshot.shape.destinationAmountRequired,
204
- destinationChain: Snapshot.shape.destinationChain,
205
- destinationToken: Snapshot.shape.destinationToken,
206
- destinationTransactionHashes: Snapshot.shape.destinationTransactionHashes,
207
- id: z
208
- .string()
209
- .check(
210
- z.regex(/^fdp_[A-Za-z0-9_-]+$/),
211
- z.describe('Funding deposit id (`fdp_…`).'),
212
- z.meta({ examples: ['fdp_001785792060000_2ZPE2gvateYEQ0dQslgvkhjx'] }),
213
- ),
214
- provider: Snapshot.shape.provider,
215
- recipient: Snapshot.shape.recipient,
216
- refundAddress: Snapshot.shape.refundAddress,
217
- refundAmount: Snapshot.shape.refundAmount,
218
- refundTransactionHashes: Snapshot.shape.refundTransactionHashes,
219
- sender: Snapshot.shape.sender,
220
- sourceAmount: Snapshot.shape.sourceAmount,
221
- sourceChain: Snapshot.shape.sourceChain,
222
- sourceToken: Snapshot.shape.sourceToken,
223
- sourceTransactionHashes: Snapshot.shape.sourceTransactionHashes,
224
- sourceTransferIndex: z
225
- .optional(z.number().check(z.int(), z.minimum(0)))
226
- .check(
227
- z.describe('Verified transfer position within the source transaction.'),
228
- z.meta({ examples: [0] }),
229
- ),
230
- status: Status,
231
- statusReason: z.optional(StatusReason),
232
- updatedAt: z
233
- .string()
234
- .check(
235
- z.describe('When the deposit last materially changed (ISO 8601).'),
236
- z.meta({ examples: ['2026-08-04T00:02:00.000Z'] }),
237
- ),
238
- })
239
- .check(z.describe('One detected deposit and its delivery status.'))
196
+ export const FundingDeposit = OpenApi.component(
197
+ z
198
+ .object({
199
+ createdAt: z.iso
200
+ .datetime()
201
+ .check(
202
+ z.describe('When the deposit was first detected (ISO 8601).'),
203
+ z.meta({ examples: ['2026-08-04T00:01:00.000Z'] }),
204
+ ),
205
+ depositAddressId: Snapshot.shape.depositAddressId,
206
+ detectionTrigger: z.optional(DetectionTrigger),
207
+ destinationAmount: Snapshot.shape.destinationAmount,
208
+ destinationAmountRequired: Snapshot.shape.destinationAmountRequired,
209
+ destinationChain: Snapshot.shape.destinationChain,
210
+ destinationToken: Snapshot.shape.destinationToken,
211
+ destinationTransactionHashes: Snapshot.shape.destinationTransactionHashes,
212
+ id: z
213
+ .string()
214
+ .check(
215
+ z.regex(/^fdp_[A-Za-z0-9_-]+$/),
216
+ z.describe('Funding deposit id (`fdp_…`).'),
217
+ z.meta({ examples: ['fdp_001785792060000_2ZPE2gvateYEQ0dQslgvkhjx'] }),
218
+ ),
219
+ provider: Snapshot.shape.provider,
220
+ recipient: Snapshot.shape.recipient,
221
+ refundAddress: Snapshot.shape.refundAddress,
222
+ refundAmount: Snapshot.shape.refundAmount,
223
+ refundTransactionHashes: Snapshot.shape.refundTransactionHashes,
224
+ sender: Snapshot.shape.sender,
225
+ sourceAmount: Snapshot.shape.sourceAmount,
226
+ sourceChain: Snapshot.shape.sourceChain,
227
+ sourceToken: Snapshot.shape.sourceToken,
228
+ sourceTransactionHashes: Snapshot.shape.sourceTransactionHashes,
229
+ sourceTransferIndex: z
230
+ .optional(z.number().check(z.int(), z.minimum(0)))
231
+ .check(
232
+ z.describe('Verified transfer position within the source transaction.'),
233
+ z.meta({ examples: [0] }),
234
+ ),
235
+ status: Status,
236
+ statusReason: z.optional(StatusReason),
237
+ updatedAt: z.iso
238
+ .datetime()
239
+ .check(
240
+ z.describe('When the deposit last materially changed (ISO 8601).'),
241
+ z.meta({ examples: ['2026-08-04T00:02:00.000Z'] }),
242
+ ),
243
+ })
244
+ .check(z.describe('One detected deposit and its delivery status.')),
245
+ 'FundingDeposit',
246
+ )
240
247
  }
241
248
 
242
249
  /** Stored public snapshot of a funding deposit. */