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
@@ -225,6 +225,10 @@ export async function get(
225
225
  return row ? toDelivery(row) : null
226
226
  }
227
227
 
228
+ // Per-statement delete bound: prune runs every minute, so a backlog drains in
229
+ // a few ticks instead of one unbounded DELETE churning the origin.
230
+ const pruneBatchLimit = 50_000
231
+
228
232
  /**
229
233
  * Deletes deliveries past their retention deadline. Called best-effort from
230
234
  * the poller tick.
@@ -236,7 +240,16 @@ export async function get(
236
240
  export async function pruneExpired(db: Db.Db, now: string): Promise<number> {
237
241
  const result = await db.kysely
238
242
  .deleteFrom('webhook_deliveries')
243
+ // Repeated on the outer delete: the row recheck under concurrency re-runs
244
+ // only this predicate, so a row renewed mid-statement survives.
239
245
  .where('expiresAt', '<=', now)
246
+ .where('id', 'in', (eb) =>
247
+ eb
248
+ .selectFrom('webhook_deliveries')
249
+ .select('id')
250
+ .where('expiresAt', '<=', now)
251
+ .limit(pruneBatchLimit),
252
+ )
240
253
  .executeTakeFirst()
241
254
  return Number(result.numDeletedRows)
242
255
  }
@@ -0,0 +1,55 @@
1
+ import type * as Webhooks from '../../internal/Webhooks.js'
2
+ import type * as Db from '../Db.js'
3
+ import type * as db_Schema from '../Schema.js'
4
+
5
+ /** Columns of the `webhook_queue_completions` table, derived from `Schema.WebhookQueueCompletion`. */
6
+ export type Table = db_Schema.WebhookQueueCompletion
7
+
8
+ /** A terminal delivery-obligation outcome. */
9
+ export type TerminalStatus = db_Schema.WebhookQueueCompletion['status']
10
+
11
+ /**
12
+ * Reads the unexpired completion for a reference, if any. Rows are written by
13
+ * the queue-event terminal transition and only ever read for dedupe.
14
+ */
15
+ export async function get(
16
+ db: Db.Db,
17
+ reference: Webhooks.QueueReference,
18
+ now: string,
19
+ ): Promise<Table | null> {
20
+ return (
21
+ (await db.kysely
22
+ .selectFrom('webhook_queue_completions')
23
+ .selectAll()
24
+ .where('eventId', '=', reference.eventId)
25
+ .where('subscriptionId', '=', reference.subscriptionId)
26
+ .where('expiresAt', '>', now)
27
+ .executeTakeFirst()) ?? null
28
+ )
29
+ }
30
+
31
+ // Per-statement delete bound: prune runs every minute, so a backlog drains in
32
+ // a few ticks instead of one unbounded DELETE churning the origin.
33
+ const pruneBatchLimit = 50_000
34
+
35
+ /** Deletes expired completions, bounded per call; expiry is filtered at query time, so pruning is space-only. */
36
+ export async function pruneExpired(db: Db.Db, now: string): Promise<number> {
37
+ const result = await db.kysely
38
+ .deleteFrom('webhook_queue_completions')
39
+ // Repeated on the outer delete: the row recheck under concurrency re-runs
40
+ // only this predicate, so a completion renewed mid-statement survives.
41
+ .where('expiresAt', '<=', now)
42
+ .where(({ eb, refTuple, selectFrom }) =>
43
+ eb(
44
+ refTuple('subscriptionId', 'eventId'),
45
+ 'in',
46
+ selectFrom('webhook_queue_completions')
47
+ .select(['subscriptionId', 'eventId'])
48
+ .where('expiresAt', '<=', now)
49
+ .limit(pruneBatchLimit)
50
+ .$asTuple('subscriptionId', 'eventId'),
51
+ ),
52
+ )
53
+ .executeTakeFirst()
54
+ return Number(result.numDeletedRows)
55
+ }
@@ -2,17 +2,20 @@ import { type ExpressionBuilder, type Generated, type Insertable, type Selectabl
2
2
 
3
3
  import type * as Webhooks from '../../internal/Webhooks.js'
4
4
  import type * as Db from '../Db.js'
5
+ import * as WebhookQueueCompletions from './webhookQueueCompletions.js'
5
6
  import type * as db_Schema from '../Schema.js'
6
7
 
7
8
  /** Columns of the `webhook_queue_events` table, derived from `Schema.WebhookQueueEvent`. */
8
9
  export type Table = Omit<
9
10
  db_Schema.WebhookQueueEvent,
10
- 'attemptCount' | 'attemptingAt' | 'nextAttemptAt' | 'observedAt' | 'status'
11
+ 'attemptCount' | 'attemptingAt' | 'expiresAt' | 'nextAttemptAt' | 'observedAt' | 'status'
11
12
  > & {
12
13
  /** Delivery attempts claimed so far; database default 0. */
13
14
  attemptCount: Generated<number>
14
15
  /** ISO timestamp of the current claim; database default null. */
15
16
  attemptingAt: Generated<string | null>
17
+ /** Legacy dedupe deadline; unwritten since completions took over. */
18
+ expiresAt: Generated<string | null>
16
19
  /** ISO retry due time; database default null (due immediately). */
17
20
  nextAttemptAt: Generated<string | null>
18
21
  /** ISO head-observation stamp; database default null (replays carry none). */
@@ -24,12 +27,9 @@ export type Table = Omit<
24
27
  /** A stored webhook Queue event. */
25
28
  export type Record = Selectable<Table>
26
29
 
27
- /** A terminal delivery-obligation outcome. */
28
- export type TerminalStatus = Exclude<db_Schema.WebhookQueueEvent['status'], 'pending'>
29
-
30
30
  /**
31
- * Reads one pending envelope. Terminal rows read as absent so no consumer
32
- * can redeliver a completed obligation or disturb its dedupe row.
31
+ * Reads one pending envelope. The status filter only ever excludes rows
32
+ * completed before the completions split; completions now leave the table.
33
33
  */
34
34
  export async function get(
35
35
  db: Db.Db,
@@ -47,42 +47,56 @@ export async function get(
47
47
  }
48
48
 
49
49
  /**
50
- * Stages envelopes as fresh obligations. Pending conflicts keep their first
51
- * envelope and backoff position; terminal conflicts inside the dedupe window
52
- * suppress the replay; terminal conflicts past it are replaced, so an expired
53
- * dedupe row pruning has not yet reclaimed never blocks a replay.
50
+ * Stages envelopes as fresh obligations. Unexpired completions and any
51
+ * existing row suppress the replay; a replay stages only when neither exists,
52
+ * which a completion past its dedupe window guarantees for completed work.
54
53
  */
55
54
  export async function ensure(db: Db.Db, rows: readonly Insertable<Table>[]): Promise<number> {
56
55
  if (rows.length === 0) return 0
56
+ const values = sql.join(
57
+ rows.map(
58
+ (row) =>
59
+ sql`(${row.subscriptionId}::text, ${row.eventId}::text, ${JSON.stringify(row.envelope)}::jsonb, ${row.createdAt}::text, ${row.nextAttemptAt ?? null}::text, ${row.observedAt ?? null}::text)`,
60
+ ),
61
+ )
62
+ // The VALUES source is raw because kysely has no table constructor for it,
63
+ // but both real tables stay in the builder so WithSchemaPlugin qualifies
64
+ // them (previews isolate by schema; raw identifiers would escape it). The
65
+ // completion check must ride the insert: a separate read would let a
66
+ // completion commit between the statements and re-stage just-finished work.
67
+ // `column1..6` are Postgres's default VALUES names, in the row order above;
68
+ // the window comparison uses each row's own staging time, not a shared clock.
69
+ // Conflicts only ever hit pending rows (or pre-split terminal rows during
70
+ // the drain), and both suppress the replay; a legitimate post-expiry replay
71
+ // meets no conflict because its completed row was deleted.
57
72
  const result = await db.kysely
58
73
  .insertInto('webhook_queue_events')
59
- .values([...rows])
60
- .onConflict((oc) =>
61
- oc
62
- .columns(['subscriptionId', 'eventId'])
63
- .doUpdateSet((eb) => ({
64
- attemptCount: 0,
65
- attemptingAt: null,
66
- createdAt: eb.ref('excluded.createdAt'),
67
- envelope: eb.ref('excluded.envelope'),
68
- expiresAt: eb.ref('excluded.expiresAt'),
69
- nextAttemptAt: eb.ref('excluded.nextAttemptAt'),
70
- // Refreshed with the rest: a replacement keeping the replaced row's
71
- // stamp would measure observation-to-attempt from an observation
72
- // that belongs to a delivery made days earlier.
73
- observedAt: eb.ref('excluded.observedAt'),
74
- status: 'pending',
75
- }))
76
- .where((eb) =>
77
- eb.and([
78
- eb('webhook_queue_events.status', '!=', 'pending'),
79
- eb('webhook_queue_events.expiresAt', '<=', eb.ref('excluded.createdAt')),
80
- ]),
74
+ .columns(['subscriptionId', 'eventId', 'envelope', 'createdAt', 'nextAttemptAt', 'observedAt'])
75
+ .expression((eb) =>
76
+ eb
77
+ .selectFrom(sql`(VALUES ${values})`.as('v'))
78
+ .select([
79
+ sql`v.column1`.as('subscriptionId'),
80
+ sql`v.column2`.as('eventId'),
81
+ sql`v.column3`.as('envelope'),
82
+ sql`v.column4`.as('createdAt'),
83
+ sql`v.column5`.as('nextAttemptAt'),
84
+ sql`v.column6`.as('observedAt'),
85
+ ])
86
+ .where(({ exists, not, selectFrom }) =>
87
+ not(
88
+ exists(
89
+ selectFrom('webhook_queue_completions')
90
+ .select('eventId')
91
+ .where('subscriptionId', '=', sql<string>`v.column1`)
92
+ .where('eventId', '=', sql<string>`v.column2`)
93
+ .where('expiresAt', '>', sql<string>`v.column4`),
94
+ ),
95
+ ),
81
96
  ),
82
97
  )
98
+ .onConflict((oc) => oc.columns(['subscriptionId', 'eventId']).doNothing())
83
99
  .executeTakeFirst()
84
- // The command tag counts inserted rows plus replaced expired rows; live
85
- // conflicts fall out of the conditional update and are not counted.
86
100
  return Number(result.numInsertedOrUpdatedRows ?? 0n)
87
101
  }
88
102
 
@@ -114,13 +128,23 @@ export async function claim(
114
128
  .executeTakeFirst()
115
129
  if (claimed) return { record: claimed, type: 'claimed' }
116
130
 
131
+ // Queue first, completions second: a complete() landing between the reads
132
+ // then still surfaces its completion, so a finished obligation never reads
133
+ // as missing, the one claim outcome alerts treat as a fault.
117
134
  const existing = await db.kysely
118
135
  .selectFrom('webhook_queue_events')
119
136
  .select(['attemptingAt', 'nextAttemptAt', 'status'])
120
137
  .where('subscriptionId', '=', reference.subscriptionId)
121
138
  .where('eventId', '=', reference.eventId)
122
139
  .executeTakeFirst()
123
- if (!existing) return { type: 'missing' }
140
+ if (!existing) {
141
+ // An unexpired completion reads as terminal so claim-outcome metrics keep
142
+ // their meaning; an expired one reads as missing, like a pruned row.
143
+ const completed = await WebhookQueueCompletions.get(db, reference, options.now)
144
+ if (completed) return { status: completed.status, type: 'terminal' }
145
+ return { type: 'missing' }
146
+ }
147
+ // Reachable only for rows completed before the completions split.
124
148
  if (existing.status !== 'pending') return { status: existing.status, type: 'terminal' }
125
149
  if (existing.attemptingAt !== null && existing.attemptingAt > options.staleBefore)
126
150
  return { type: 'attempting' }
@@ -142,12 +166,12 @@ export declare namespace claim {
142
166
  | { record: Record; type: 'claimed' }
143
167
  | { type: 'missing' }
144
168
  | { nextAttemptAt: string | null; type: 'scheduled' }
145
- | { status: TerminalStatus; type: 'terminal' }
169
+ | { status: WebhookQueueCompletions.TerminalStatus; type: 'terminal' }
146
170
  }
147
171
 
148
172
  /**
149
- * Marks a claimed obligation terminal, starting its dedupe retention window;
150
- * pending rows never expire, so this stamp is the only expiry that matters.
173
+ * Marks a claimed obligation terminal: one atomic statement deletes the queue
174
+ * row and writes the insert-only completion that starts the dedupe window.
151
175
  * The `attemptingAt` predicate fences stale claimants.
152
176
  */
153
177
  export async function complete(
@@ -156,14 +180,38 @@ export async function complete(
156
180
  options: complete.Options,
157
181
  ): Promise<boolean> {
158
182
  const result = await db.kysely
159
- .updateTable('webhook_queue_events')
160
- .set({ attemptingAt: null, expiresAt: options.expiresAt, status: options.status })
161
- .where('subscriptionId', '=', reference.subscriptionId)
162
- .where('eventId', '=', reference.eventId)
163
- .where('status', '=', 'pending')
164
- .where('attemptingAt', '=', options.claimedAt)
183
+ .with('done', (qb) =>
184
+ qb
185
+ .deleteFrom('webhook_queue_events')
186
+ .where('subscriptionId', '=', reference.subscriptionId)
187
+ .where('eventId', '=', reference.eventId)
188
+ .where('status', '=', 'pending')
189
+ .where('attemptingAt', '=', options.claimedAt)
190
+ .returning(['subscriptionId', 'eventId']),
191
+ )
192
+ .insertInto('webhook_queue_completions')
193
+ .columns(['subscriptionId', 'eventId', 'status', 'expiresAt'])
194
+ .expression((eb) =>
195
+ eb
196
+ .selectFrom('done')
197
+ .select((s) => [
198
+ 'subscriptionId',
199
+ 'eventId',
200
+ s.val(options.status).as('status'),
201
+ s.val(options.expiresAt).as('expiresAt'),
202
+ ]),
203
+ )
204
+ // Re-completing a replayed obligation: the latest delivery outcome wins
205
+ // and retention restarts, matching pre-split replay semantics; doNothing
206
+ // would also report zero rows and fail a fence that won.
207
+ .onConflict((oc) =>
208
+ oc.columns(['subscriptionId', 'eventId']).doUpdateSet((eb) => ({
209
+ expiresAt: eb.ref('excluded.expiresAt'),
210
+ status: eb.ref('excluded.status'),
211
+ })),
212
+ )
165
213
  .executeTakeFirst()
166
- return result.numUpdatedRows === 1n
214
+ return result.numInsertedOrUpdatedRows === 1n
167
215
  }
168
216
 
169
217
  export declare namespace complete {
@@ -171,10 +219,10 @@ export declare namespace complete {
171
219
  type Options = {
172
220
  /** Claim marker returned by {@link claim}; fences stale claimants. */
173
221
  claimedAt: string
174
- /** Replacement ISO retention deadline for the terminal dedupe row. */
222
+ /** ISO retention deadline for the dedupe completion. */
175
223
  expiresAt: string
176
224
  /** Terminal outcome. */
177
- status: TerminalStatus
225
+ status: WebhookQueueCompletions.TerminalStatus
178
226
  }
179
227
  }
180
228
 
@@ -275,17 +323,3 @@ export declare namespace pending {
275
323
  oldestDueAt: string | null
276
324
  }
277
325
  }
278
-
279
- /**
280
- * Deletes terminal dedupe rows past their retention deadline. Pending rows
281
- * never expire: owed work drains only through terminal transitions
282
- * (delivery, permanent failure, or the skip after auto-disable).
283
- */
284
- export async function pruneExpired(db: Db.Db, now: string): Promise<number> {
285
- const result = await db.kysely
286
- .deleteFrom('webhook_queue_events')
287
- .where('expiresAt', '<=', now)
288
- .where('status', '!=', 'pending')
289
- .executeTakeFirst()
290
- return Number(result.numDeletedRows)
291
- }
@@ -447,37 +447,57 @@ export async function getCursor(db: Db.Db, id: string): Promise<string | null> {
447
447
 
448
448
  /**
449
449
  * Records a successful delivery: resets the consecutive-failure counter and
450
- * stamps `lastDeliveryAt`.
450
+ * stamps `lastDeliveryAt`. Skipped when neither field would move, so
451
+ * deliveries stop contending on the row's lock.
451
452
  *
452
453
  * @param db - The database.
453
454
  * @param id - The subscription id (`wh_…`).
454
- * @param now - Delivery timestamp (ISO).
455
- * @returns The updated subscription, or `undefined` when the row is absent.
455
+ * @param options - Delivery timestamp and stamp-staleness bound.
456
+ * @returns The updated subscription, or `undefined` when nothing was written.
456
457
  */
457
458
  export async function recordSuccess(
458
459
  db: Db.Db,
459
460
  id: string,
460
- now: string,
461
+ options: recordSuccess.Options,
461
462
  ): Promise<Webhooks.Subscription | undefined> {
463
+ const { now, staleBefore } = options
462
464
  const row = await db.kysely
463
465
  .updateTable('webhook_subscriptions')
464
466
  .set({ failureCount: 0, lastDeliveryAt: now, updatedAt: now })
465
467
  .where('id', '=', id)
468
+ .where((eb) =>
469
+ eb.or([
470
+ eb('failureCount', '>', 0),
471
+ eb('lastDeliveryAt', 'is', null),
472
+ eb('lastDeliveryAt', '<', staleBefore),
473
+ ]),
474
+ )
466
475
  .returningAll()
467
476
  .executeTakeFirst()
468
477
  return row ? toSubscription(row) : undefined
469
478
  }
470
479
 
480
+ export declare namespace recordSuccess {
481
+ /** Clock bounds for one success record. */
482
+ type Options = {
483
+ /** Delivery timestamp (ISO). */
484
+ now: string
485
+ /** Rewrite the stamp only when it predates this (ISO). */
486
+ staleBefore: string
487
+ }
488
+ }
489
+
471
490
  /**
472
491
  * Records a failed delivery: atomically increments the consecutive-failure
473
492
  * counter and disables the subscription once it reaches `maxFailures` (raw
474
493
  * fragments use storage column names — they bypass the camelCase mapping).
494
+ * Skipped once disabled at the cap, when nothing would move.
475
495
  *
476
496
  * @param db - The database.
477
497
  * @param id - The subscription id (`wh_…`).
478
498
  * @param maxFailures - Consecutive failures before auto-disabling.
479
499
  * @param now - Failure timestamp (ISO).
480
- * @returns The updated subscription, or `undefined` when the row is absent.
500
+ * @returns The updated subscription, or `undefined` when nothing was written.
481
501
  */
482
502
  export async function recordFailure(
483
503
  db: Db.Db,
@@ -493,11 +513,19 @@ export async function recordFailure(
493
513
  updatedAt: now,
494
514
  })
495
515
  .where('id', '=', id)
516
+ // In-flight failures race the disable and would queue on the row lock. A
517
+ // re-enabled subscription keeps its capped counter; the status arm lets
518
+ // its next failure re-disable it.
519
+ .where((eb) => eb.or([eb('failureCount', '<', maxFailures), eb('status', '!=', 'disabled')]))
496
520
  .returningAll()
497
521
  .executeTakeFirst()
498
522
  return row ? toSubscription(row) : undefined
499
523
  }
500
524
 
525
+ // Smaller than the ledger-table bounds: each subscription delete cascades
526
+ // into deliveries, queue events, and completions, fanning the statement out.
527
+ const pruneBatchLimit = 1_000
528
+
501
529
  /**
502
530
  * Deletes expired subscriptions (their delivery rows cascade). Called
503
531
  * best-effort on a schedule.
@@ -507,10 +535,21 @@ export async function recordFailure(
507
535
  * @returns The number of rows deleted.
508
536
  */
509
537
  export async function pruneExpired(db: Db.Db, now: string): Promise<number> {
538
+ // Bounded like the ledger prunes: this delete cascades into deliveries,
539
+ // queue events, and completions, so it fans out the widest of the four.
510
540
  const result = await db.kysely
511
541
  .deleteFrom('webhook_subscriptions')
512
- .where('expiresAt', 'is not', null)
542
+ // Repeated on the outer delete: the row recheck under concurrency re-runs
543
+ // only this predicate, so a subscription renewed mid-statement survives.
513
544
  .where('expiresAt', '<=', now)
545
+ .where('id', 'in', (eb) =>
546
+ eb
547
+ .selectFrom('webhook_subscriptions')
548
+ .select('id')
549
+ .where('expiresAt', 'is not', null)
550
+ .where('expiresAt', '<=', now)
551
+ .limit(pruneBatchLimit),
552
+ )
514
553
  .executeTakeFirst()
515
554
  return Number(result.numDeletedRows)
516
555
  }
@@ -587,11 +587,21 @@ export function middleware<
587
587
  const overrides = options.overrides as
588
588
  | Record<string, PolicyOverride | false | undefined>
589
589
  | undefined
590
- const configuredOverride = overrides?.[key]
590
+ const routePolicies = policies(c)
591
+ const configuredOverride = (() => {
592
+ const configured = overrides?.[key]
593
+ if (configured !== undefined) return configured
594
+ const inheritedMethod = routePolicies.find(
595
+ (policy) => policy.inheritOverridesFrom,
596
+ )?.inheritOverridesFrom
597
+ if (inheritedMethod === undefined) return undefined
598
+ const inheritedKey = `${inheritedMethod.toUpperCase()} ${key.slice(key.indexOf(' ') + 1)}`
599
+ return overrides?.[inheritedKey]
600
+ })()
591
601
  const routeOverride =
592
602
  management && configuredOverride ? { mpp: configuredOverride.mpp } : configuredOverride
593
603
  const endpointPolicy = bindMppScope(
594
- resolvePolicy(base, defaults, [...policies(c), routeOverride], description),
604
+ resolvePolicy(base, defaults, [...routePolicies, routeOverride], description),
595
605
  scope,
596
606
  )
597
607
  if (!endpointPolicy) return next()
@@ -1000,12 +1010,20 @@ export declare namespace install {
1000
1010
  }
1001
1011
 
1002
1012
  /** Creates route middleware that contributes an auth policy override. */
1003
- export function policy(options: PolicyOverride): MiddlewareHandler {
1013
+ export function policy(options: policy.Options): MiddlewareHandler {
1004
1014
  const middleware = createMiddleware((_, next) => next()) as MiddlewareHandler & PolicyHandler
1005
1015
  middleware[policySymbol] = options
1006
1016
  return middleware
1007
1017
  }
1008
1018
 
1019
+ export declare namespace policy {
1020
+ /** Route policy plus metadata for compatibility aliases. */
1021
+ type Options = PolicyOverride & {
1022
+ /** HTTP method whose configured override this route inherits when its own is absent. */
1023
+ inheritOverridesFrom?: string | undefined
1024
+ }
1025
+ }
1026
+
1009
1027
  /** Adds management POSTs for OpenAPI-registered MPP GET resources. */
1010
1028
  export function installMppManagementRoutes(
1011
1029
  app: Hono<any, any>,
@@ -1106,7 +1124,7 @@ type OpenApiRoute = {
1106
1124
  }
1107
1125
 
1108
1126
  type PolicyHandler = {
1109
- [policySymbol]?: PolicyOverride | undefined
1127
+ [policySymbol]?: policy.Options | undefined
1110
1128
  }
1111
1129
 
1112
1130
  type MppManagementHandler = {
@@ -1283,7 +1301,7 @@ function policies(c: hono_Context) {
1283
1301
  return matchedRoutes(c)
1284
1302
  .slice(c.req.routeIndex + 1)
1285
1303
  .map(policyFromRoute)
1286
- .filter((policy): policy is PolicyOverride => policy !== undefined)
1304
+ .filter((routePolicy): routePolicy is policy.Options => routePolicy !== undefined)
1287
1305
  }
1288
1306
 
1289
1307
  function openApiSpec(route: RouterRoute) {
@@ -58,10 +58,8 @@ export function response<environment extends Env>(
58
58
  ): MiddlewareHandler<environment> {
59
59
  const defaultCacheControl = options.cacheControl ?? policies.default
60
60
  const vary = options.vary ?? ['Accept-Encoding', 'Authorization', 'Tempo-API-Key', 'X-API-Key']
61
- // hono/cache suffixes its key with `::<directive>=<value>` for every
62
- // configured vary directive (trimmed, lowercased, deduped). Mirror that
63
- // normalization so the `no-cache` purge below deletes exactly the entry the
64
- // wrapped middleware reads — drift here would make refreshes silently miss.
61
+ // Mirror hono/cache's vary normalization so the `no-cache` purge below
62
+ // deletes exactly the entry the wrapped middleware reads.
65
63
  const varyKeyDirectives = [
66
64
  ...new Set(vary.map((name) => name.trim().toLowerCase()).filter(Boolean)),
67
65
  ]
@@ -95,10 +93,19 @@ export function response<environment extends Env>(
95
93
  type Caches = { open(name: string): Promise<{ delete(key: string): Promise<unknown> }> }
96
94
  const caches = (globalThis as { caches?: Caches }).caches
97
95
  if (noCache && !noStore && caches) {
98
- let key = await options.key(c)
96
+ // hono/cache 4.13 wraps generated keys in a canonical URL containing the
97
+ // request method and vary values. Keep this in sync with its
98
+ // `createCacheKey` so refreshes remove the stored GET response.
99
+ const key = await options.key(c)
100
+ const url = new URL('/.hono/cache', c.req.url)
101
+ url.searchParams.append('__hono_cache_key', key.split('#', 1)[0] ?? key)
102
+ url.searchParams.append('__hono_cache_method', c.req.method)
99
103
  for (const name of varyKeyDirectives)
100
- key += `::${name}=${encodeURIComponent(c.req.raw.headers.get(name) ?? '')}`
101
- await (await caches.open(options.name)).delete(key)
104
+ url.searchParams.append(
105
+ '__hono_cache_vary',
106
+ JSON.stringify([name, c.req.raw.headers.get(name) ?? '']),
107
+ )
108
+ await (await caches.open(options.name)).delete(url.href)
102
109
  }
103
110
 
104
111
  // Run ETag inside Hono's cache miss path so fresh responses are hashed
@@ -0,0 +1,33 @@
1
+ import * as EarnVaults from './EarnVaults.js'
2
+
3
+ describe('addZoneRouteCapabilities', () => {
4
+ test('adds private routing and router swaps', () => {
5
+ expect(
6
+ EarnVaults.addZoneRouteCapabilities({
7
+ asyncRedeem: false,
8
+ boundedRedeem: true,
9
+ deposit: true,
10
+ exactWithdraw: true,
11
+ inKindDeposit: true,
12
+ privateRouting: false,
13
+ redeem: true,
14
+ routerSwaps: false,
15
+ }),
16
+ ).toMatchObject({ privateRouting: true, routerSwaps: true })
17
+ })
18
+ })
19
+
20
+ describe('deriveZoneId', () => {
21
+ test('derives IDs from mainnet and testnet Zone chain schemes', () => {
22
+ expect([
23
+ EarnVaults.deriveZoneId(421_700_006),
24
+ EarnVaults.deriveZoneId(1_424_310_001),
25
+ ]).toStrictEqual([6, 1])
26
+ })
27
+
28
+ test('rejects a source chain ID', () => {
29
+ expect(() => EarnVaults.deriveZoneId(31_318)).toThrowErrorMatchingInlineSnapshot(
30
+ `[EarnVaults.InvalidRegistryError: Chain 31318 does not use a supported Zone chain-ID scheme.]`,
31
+ )
32
+ })
33
+ })
@@ -1,6 +1,7 @@
1
1
  import { parseAbi } from 'viem'
2
2
  import { tempoTestnet } from 'viem/tempo/chains'
3
3
 
4
+ import * as TestEarn from '../../test/Earn.js'
4
5
  import * as Runtime from '../../test/runtime.js'
5
6
  import * as TestViem from '../../test/Viem.js'
6
7
  import * as Zones from '../apps/Zones.js'
@@ -43,6 +44,17 @@ describe('schema.ResolveInput', () => {
43
44
  })
44
45
  })
45
46
 
47
+ describe('schema.ZoneRoute', () => {
48
+ test('normalizes the router address', () => {
49
+ expect(
50
+ EarnVaults.schema.ZoneRoute.parse({
51
+ ...TestEarn.zoneRoute,
52
+ earnRouter: TestEarn.zoneRoute.earnRouter.toUpperCase().replace('0X', '0x'),
53
+ }),
54
+ ).toStrictEqual(TestEarn.zoneRoute)
55
+ })
56
+ })
57
+
46
58
  describe('classifyEngineType', () => {
47
59
  test('classifies supported capability fingerprints', () => {
48
60
  expect([
@@ -193,3 +205,70 @@ describe('resolve', () => {
193
205
  60_000,
194
206
  )
195
207
  })
208
+
209
+ describe('resolveZoneRoutes', () => {
210
+ test.runIf(Runtime.get().mode === 'testnet')(
211
+ 'verifies immutable router bindings and derives private tokens',
212
+ async () => {
213
+ const getClient = Viem.createGetClient({
214
+ defaultChainId: tempoTestnet.id,
215
+ rpc: TestViem.rpc,
216
+ zones: [TestEarn.zone],
217
+ })
218
+ const discovery = await EarnVaults.resolve({
219
+ getClient,
220
+ input: {
221
+ chainId: tempoTestnet.id,
222
+ vaultAddress: TestEarn.zoneVaultAddress,
223
+ },
224
+ zones: [TestEarn.zone],
225
+ })
226
+ const routes = await EarnVaults.resolveZoneRoutes({
227
+ discovery,
228
+ getClient,
229
+ routes: [TestEarn.zoneRoute],
230
+ zones: [TestEarn.zone],
231
+ })
232
+
233
+ expect(routes).toHaveLength(1)
234
+ expect(routes[0]).toMatchObject({
235
+ ...TestEarn.zoneRoute,
236
+ name: TestEarn.zone.name,
237
+ })
238
+ expect(routes[0]?.inputTokens).toHaveLength(1)
239
+ expect(routes[0]?.outputTokens).toStrictEqual(routes[0]?.inputTokens)
240
+ await expect(
241
+ EarnVaults.resolveZoneRoutes({
242
+ discovery,
243
+ getClient,
244
+ routes: [TestEarn.zoneRoute, TestEarn.zoneRoute],
245
+ zones: [TestEarn.zone],
246
+ }),
247
+ ).rejects.toThrowErrorMatchingInlineSnapshot(
248
+ `[EarnVaults.InvalidRegistryError: Zone 1424310001 is configured more than once.]`,
249
+ )
250
+ await expect(
251
+ EarnVaults.resolveZoneRoutes({
252
+ discovery,
253
+ getClient,
254
+ routes: [
255
+ { ...TestEarn.zoneRoute, deploymentBlock: TestEarn.zoneRoute.deploymentBlock + 1 },
256
+ ],
257
+ zones: [TestEarn.zone],
258
+ }),
259
+ ).rejects.toMatchObject({
260
+ result: {
261
+ issues: [
262
+ {
263
+ actual: String(TestEarn.zoneRoute.deploymentBlock + 1),
264
+ code: 'zone-deployment-block-mismatch',
265
+ message: 'Earn router deployment block does not match its code boundary.',
266
+ },
267
+ ],
268
+ status: 'mismatch',
269
+ },
270
+ })
271
+ },
272
+ 60_000,
273
+ )
274
+ })