zklicensing 0.1.0-beta.2 → 0.1.0-beta.21

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 (317) hide show
  1. package/README.md +417 -201
  2. package/build/agent.d.ts +371 -0
  3. package/build/agent.d.ts.map +1 -0
  4. package/build/agent.js +553 -0
  5. package/build/agent.js.map +1 -0
  6. package/build/agentActions.d.ts +841 -0
  7. package/build/agentActions.d.ts.map +1 -0
  8. package/build/agentActions.js +581 -0
  9. package/build/agentActions.js.map +1 -0
  10. package/build/agentRecord.d.ts +83 -0
  11. package/build/agentRecord.d.ts.map +1 -0
  12. package/build/agentRecord.js +69 -0
  13. package/build/agentRecord.js.map +1 -0
  14. package/build/agentVendor.d.ts +220 -0
  15. package/build/agentVendor.d.ts.map +1 -0
  16. package/build/agentVendor.js +306 -0
  17. package/build/agentVendor.js.map +1 -0
  18. package/build/appsLookup.d.ts +14 -0
  19. package/build/appsLookup.d.ts.map +1 -0
  20. package/build/appsLookup.js +12 -0
  21. package/build/appsLookup.js.map +1 -0
  22. package/build/archiveBootstrap.d.ts +31 -5
  23. package/build/archiveBootstrap.d.ts.map +1 -0
  24. package/build/archiveBootstrap.js +140 -47
  25. package/build/archiveBootstrap.js.map +1 -0
  26. package/build/atomicJson.d.ts +5 -0
  27. package/build/atomicJson.d.ts.map +1 -0
  28. package/build/atomicJson.js +55 -0
  29. package/build/atomicJson.js.map +1 -0
  30. package/build/bindClient.d.ts +85 -0
  31. package/build/bindClient.d.ts.map +1 -0
  32. package/build/bindClient.js +187 -0
  33. package/build/bindClient.js.map +1 -0
  34. package/build/buyClient.d.ts +27 -4
  35. package/build/buyClient.d.ts.map +1 -0
  36. package/build/buyClient.js +72 -26
  37. package/build/buyClient.js.map +1 -0
  38. package/build/chainHead.d.ts +61 -0
  39. package/build/chainHead.d.ts.map +1 -0
  40. package/build/chainHead.js +88 -0
  41. package/build/chainHead.js.map +1 -0
  42. package/build/compactMerkleMap.d.ts +72 -0
  43. package/build/compactMerkleMap.d.ts.map +1 -0
  44. package/build/compactMerkleMap.js +224 -0
  45. package/build/compactMerkleMap.js.map +1 -0
  46. package/build/contractInterface.d.ts +191 -51
  47. package/build/contractInterface.d.ts.map +1 -0
  48. package/build/contractInterface.js +234 -43
  49. package/build/contractInterface.js.map +1 -0
  50. package/build/contractTypes.d.ts +9 -0
  51. package/build/contractTypes.d.ts.map +1 -0
  52. package/build/contractTypes.js +28 -0
  53. package/build/contractTypes.js.map +1 -0
  54. package/build/countryStats.d.ts +12 -3
  55. package/build/countryStats.d.ts.map +1 -0
  56. package/build/countryStats.js +87 -23
  57. package/build/countryStats.js.map +1 -0
  58. package/build/deployClient.d.ts +138 -24
  59. package/build/deployClient.d.ts.map +1 -0
  60. package/build/deployClient.js +91 -31
  61. package/build/deployClient.js.map +1 -0
  62. package/build/deviceIdentity.d.ts +68 -0
  63. package/build/deviceIdentity.d.ts.map +1 -0
  64. package/build/deviceIdentity.js +209 -0
  65. package/build/deviceIdentity.js.map +1 -0
  66. package/build/freezeClient.d.ts +26 -6
  67. package/build/freezeClient.d.ts.map +1 -0
  68. package/build/freezeClient.js +30 -6
  69. package/build/freezeClient.js.map +1 -0
  70. package/build/generationClient.d.ts +6 -3
  71. package/build/generationClient.d.ts.map +1 -0
  72. package/build/generationClient.js +25 -38
  73. package/build/generationClient.js.map +1 -0
  74. package/build/holderSecret.d.ts +70 -0
  75. package/build/holderSecret.d.ts.map +1 -0
  76. package/build/holderSecret.js +76 -0
  77. package/build/holderSecret.js.map +1 -0
  78. package/build/index.d.ts +38 -38
  79. package/build/index.d.ts.map +1 -0
  80. package/build/index.js +97 -121
  81. package/build/index.js.map +1 -0
  82. package/build/keysClient.d.ts +142 -0
  83. package/build/keysClient.d.ts.map +1 -0
  84. package/build/keysClient.js +109 -0
  85. package/build/keysClient.js.map +1 -0
  86. package/build/licenseLeaf.d.ts +76 -0
  87. package/build/licenseLeaf.d.ts.map +1 -0
  88. package/build/licenseLeaf.js +189 -0
  89. package/build/licenseLeaf.js.map +1 -0
  90. package/build/licenseRecord.d.ts +68 -5
  91. package/build/licenseRecord.d.ts.map +1 -0
  92. package/build/licenseRecord.js +26 -1
  93. package/build/licenseRecord.js.map +1 -0
  94. package/build/licenseStore.d.ts +81 -4
  95. package/build/licenseStore.d.ts.map +1 -0
  96. package/build/licenseStore.js +400 -30
  97. package/build/licenseStore.js.map +1 -0
  98. package/build/licenseTrustless.d.ts +43 -0
  99. package/build/licenseTrustless.d.ts.map +1 -0
  100. package/build/licenseTrustless.js +180 -0
  101. package/build/licenseTrustless.js.map +1 -0
  102. package/build/manifestVerify.d.ts +29 -1
  103. package/build/manifestVerify.d.ts.map +1 -0
  104. package/build/manifestVerify.js +46 -4
  105. package/build/manifestVerify.js.map +1 -0
  106. package/build/minaPriceHistory.d.ts +9 -0
  107. package/build/minaPriceHistory.d.ts.map +1 -0
  108. package/build/minaPriceHistory.js +152 -0
  109. package/build/minaPriceHistory.js.map +1 -0
  110. package/build/onChainRoot.d.ts +60 -0
  111. package/build/onChainRoot.d.ts.map +1 -0
  112. package/build/onChainRoot.js +74 -0
  113. package/build/onChainRoot.js.map +1 -0
  114. package/build/ownershipLicenseHash.d.ts +25 -0
  115. package/build/ownershipLicenseHash.d.ts.map +1 -0
  116. package/build/ownershipLicenseHash.js +29 -0
  117. package/build/ownershipLicenseHash.js.map +1 -0
  118. package/build/ownershipSignature.d.ts +153 -0
  119. package/build/ownershipSignature.d.ts.map +1 -0
  120. package/build/ownershipSignature.js +297 -0
  121. package/build/ownershipSignature.js.map +1 -0
  122. package/build/permitCheckFlow.d.ts +155 -0
  123. package/build/permitCheckFlow.d.ts.map +1 -0
  124. package/build/permitCheckFlow.js +196 -0
  125. package/build/permitCheckFlow.js.map +1 -0
  126. package/build/permitDescribe.d.ts +124 -0
  127. package/build/permitDescribe.d.ts.map +1 -0
  128. package/build/permitDescribe.js +324 -0
  129. package/build/permitDescribe.js.map +1 -0
  130. package/build/permitInterface.d.ts +85 -0
  131. package/build/permitInterface.d.ts.map +1 -0
  132. package/build/permitInterface.js +121 -0
  133. package/build/permitInterface.js.map +1 -0
  134. package/build/permitMessages.d.ts +313 -0
  135. package/build/permitMessages.d.ts.map +1 -0
  136. package/build/permitMessages.js +462 -0
  137. package/build/permitMessages.js.map +1 -0
  138. package/build/permitProof.d.ts +108 -0
  139. package/build/permitProof.d.ts.map +1 -0
  140. package/build/permitProof.js +202 -0
  141. package/build/permitProof.js.map +1 -0
  142. package/build/permitRegistryRebuild.d.ts +166 -0
  143. package/build/permitRegistryRebuild.d.ts.map +1 -0
  144. package/build/permitRegistryRebuild.js +347 -0
  145. package/build/permitRegistryRebuild.js.map +1 -0
  146. package/build/permitTree.d.ts +88 -0
  147. package/build/permitTree.d.ts.map +1 -0
  148. package/build/permitTree.js +230 -0
  149. package/build/permitTree.js.map +1 -0
  150. package/build/permitsClient.d.ts +680 -0
  151. package/build/permitsClient.d.ts.map +1 -0
  152. package/build/permitsClient.js +597 -0
  153. package/build/permitsClient.js.map +1 -0
  154. package/build/proveErrors.d.ts +50 -0
  155. package/build/proveErrors.d.ts.map +1 -0
  156. package/build/proveErrors.js +76 -0
  157. package/build/proveErrors.js.map +1 -0
  158. package/build/receipt.d.ts +95 -0
  159. package/build/receipt.d.ts.map +1 -0
  160. package/build/receipt.js +84 -0
  161. package/build/receipt.js.map +1 -0
  162. package/build/refundClient.d.ts +7 -12
  163. package/build/refundClient.d.ts.map +1 -0
  164. package/build/refundClient.js +52 -21
  165. package/build/refundClient.js.map +1 -0
  166. package/build/registryTrustVerify.d.ts +78 -0
  167. package/build/registryTrustVerify.d.ts.map +1 -0
  168. package/build/registryTrustVerify.js +129 -0
  169. package/build/registryTrustVerify.js.map +1 -0
  170. package/build/releaseClient.d.ts +26 -0
  171. package/build/releaseClient.d.ts.map +1 -0
  172. package/build/releaseClient.js +65 -0
  173. package/build/releaseClient.js.map +1 -0
  174. package/build/renewClient.d.ts +28 -2
  175. package/build/renewClient.d.ts.map +1 -0
  176. package/build/renewClient.js +31 -27
  177. package/build/renewClient.js.map +1 -0
  178. package/build/seatName.d.ts +8 -0
  179. package/build/seatName.d.ts.map +1 -0
  180. package/build/seatName.js +136 -0
  181. package/build/seatName.js.map +1 -0
  182. package/build/slotConstants.d.ts +15 -0
  183. package/build/slotConstants.d.ts.map +1 -0
  184. package/build/slotConstants.js +17 -0
  185. package/build/slotConstants.js.map +1 -0
  186. package/build/statsClient.d.ts +61 -7
  187. package/build/statsClient.d.ts.map +1 -0
  188. package/build/statsClient.js +44 -5
  189. package/build/statsClient.js.map +1 -0
  190. package/build/successorChain.d.ts +35 -0
  191. package/build/successorChain.d.ts.map +1 -0
  192. package/build/successorChain.js +69 -0
  193. package/build/successorChain.js.map +1 -0
  194. package/build/taxId.d.ts +44 -0
  195. package/build/taxId.d.ts.map +1 -0
  196. package/build/taxId.js +212 -0
  197. package/build/taxId.js.map +1 -0
  198. package/build/trustAnchorVerify.d.ts +49 -0
  199. package/build/trustAnchorVerify.d.ts.map +1 -0
  200. package/build/trustAnchorVerify.js +432 -0
  201. package/build/trustAnchorVerify.js.map +1 -0
  202. package/build/trustAnchorVerifyCli.d.ts +45 -0
  203. package/build/trustAnchorVerifyCli.d.ts.map +1 -0
  204. package/build/trustAnchorVerifyCli.js +235 -0
  205. package/build/trustAnchorVerifyCli.js.map +1 -0
  206. package/build/txBroadcastClient.d.ts +26 -0
  207. package/build/txBroadcastClient.d.ts.map +1 -0
  208. package/build/txBroadcastClient.js +20 -0
  209. package/build/txBroadcastClient.js.map +1 -0
  210. package/build/unbindClient.d.ts +77 -0
  211. package/build/unbindClient.d.ts.map +1 -0
  212. package/build/unbindClient.js +169 -0
  213. package/build/unbindClient.js.map +1 -0
  214. package/build/units.d.ts +28 -0
  215. package/build/units.d.ts.map +1 -0
  216. package/build/units.js +65 -0
  217. package/build/units.js.map +1 -0
  218. package/build/vatRates.d.ts +54 -0
  219. package/build/vatRates.d.ts.map +1 -0
  220. package/build/vatRates.js +82 -0
  221. package/build/vatRates.js.map +1 -0
  222. package/build/vatRatesData.d.ts +5 -0
  223. package/build/vatRatesData.d.ts.map +1 -0
  224. package/build/vatRatesData.js +42 -0
  225. package/build/vatRatesData.js.map +1 -0
  226. package/build/vendorAccountClient.d.ts +66 -0
  227. package/build/vendorAccountClient.d.ts.map +1 -0
  228. package/build/vendorAccountClient.js +47 -0
  229. package/build/vendorAccountClient.js.map +1 -0
  230. package/build/verifyCore.d.ts +4 -1
  231. package/build/verifyCore.d.ts.map +1 -0
  232. package/build/verifyCore.js +21 -10
  233. package/build/verifyCore.js.map +1 -0
  234. package/build/verifyOffline.d.ts +85 -0
  235. package/build/verifyOffline.d.ts.map +1 -0
  236. package/build/verifyOffline.js +139 -0
  237. package/build/verifyOffline.js.map +1 -0
  238. package/build/verifyService.d.ts.map +1 -0
  239. package/build/verifyService.js +571 -501
  240. package/build/verifyService.js.map +1 -0
  241. package/build/verifyWithAncestors.d.ts +19 -0
  242. package/build/verifyWithAncestors.d.ts.map +1 -0
  243. package/build/verifyWithAncestors.js +127 -0
  244. package/build/verifyWithAncestors.js.map +1 -0
  245. package/build/zipToState.d.ts +2 -0
  246. package/build/zipToState.d.ts.map +1 -0
  247. package/build/zipToState.js +94 -0
  248. package/build/zipToState.js.map +1 -0
  249. package/package.json +141 -16
  250. package/src/agent.ts +682 -0
  251. package/src/agentActions.ts +637 -0
  252. package/src/agentRecord.ts +121 -0
  253. package/src/agentVendor.ts +356 -0
  254. package/src/appsLookup.ts +48 -0
  255. package/src/archiveBootstrap.ts +386 -0
  256. package/src/atomicJson.ts +53 -0
  257. package/src/bindClient.ts +297 -0
  258. package/src/buyClient.ts +344 -0
  259. package/src/chainHead.ts +106 -0
  260. package/src/compactMerkleMap.ts +222 -0
  261. package/src/contractInterface.ts +465 -0
  262. package/src/contractTypes.ts +31 -0
  263. package/src/countryStats.ts +200 -0
  264. package/src/deployClient.ts +517 -0
  265. package/src/deviceIdentity.ts +249 -0
  266. package/src/freezeClient.ts +162 -0
  267. package/src/generationClient.ts +145 -0
  268. package/src/holderSecret.ts +91 -0
  269. package/src/index.ts +362 -0
  270. package/src/keysClient.ts +135 -0
  271. package/src/licenseLeaf.ts +221 -0
  272. package/src/licenseRecord.ts +190 -0
  273. package/src/licenseStore.ts +767 -0
  274. package/src/licenseTrustless.ts +207 -0
  275. package/src/manifestVerify.ts +164 -0
  276. package/src/minaPriceHistory.ts +166 -0
  277. package/src/onChainRoot.ts +90 -0
  278. package/src/ownershipLicenseHash.ts +31 -0
  279. package/src/ownershipSignature.ts +421 -0
  280. package/src/permitCheckFlow.ts +227 -0
  281. package/src/permitDescribe.ts +362 -0
  282. package/src/permitInterface.ts +146 -0
  283. package/src/permitMessages.ts +572 -0
  284. package/src/permitProof.ts +230 -0
  285. package/src/permitRegistryRebuild.ts +423 -0
  286. package/src/permitTree.ts +232 -0
  287. package/src/permitsClient.ts +746 -0
  288. package/src/proveErrors.ts +98 -0
  289. package/src/receipt.ts +156 -0
  290. package/src/refundClient.ts +177 -0
  291. package/src/registryTrustVerify.ts +192 -0
  292. package/src/releaseClient.ts +101 -0
  293. package/src/renewClient.ts +234 -0
  294. package/src/seatName.ts +165 -0
  295. package/src/slotConstants.ts +18 -0
  296. package/src/statsClient.ts +249 -0
  297. package/src/successorChain.ts +87 -0
  298. package/src/taxId.ts +227 -0
  299. package/src/trustAnchorVerify.ts +550 -0
  300. package/src/trustAnchorVerifyCli.ts +234 -0
  301. package/src/txBroadcastClient.ts +48 -0
  302. package/src/unbindClient.ts +233 -0
  303. package/src/units.ts +74 -0
  304. package/src/vatRates.ts +100 -0
  305. package/src/vatRatesData.ts +43 -0
  306. package/src/vendorAccountClient.ts +120 -0
  307. package/src/verifyCore.ts +100 -0
  308. package/src/verifyOffline.ts +212 -0
  309. package/src/verifyService.ts +982 -0
  310. package/src/verifyWithAncestors.ts +163 -0
  311. package/src/zipToState.ts +94 -0
  312. package/build/licenseProof.d.ts +0 -26
  313. package/build/licenseProof.js +0 -41
  314. package/build/migrateClient.d.ts +0 -43
  315. package/build/migrateClient.js +0 -47
  316. package/build/ownership.d.ts +0 -34
  317. package/build/ownership.js +0 -147
@@ -0,0 +1,517 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * deployClient.ts
6
+ *
7
+ * Browser-safe client for the deploy flow. Mirrors the buy / renew / refund
8
+ * client shape: `proveDeploy` posts to the backend's `/prove/deploy` endpoint
9
+ * and returns the atomic deploy+initialize tx already signed with the
10
+ * throwaway zkApp key. The vendor's wallet then adds the fee-payer signature
11
+ * and broadcasts.
12
+ *
13
+ * The atomic bundling is what makes single-shot initialization safe:
14
+ * `provenTxJson` holds both `zkApp.deploy()` and `zkApp.initialize()`
15
+ * AccountUpdates in the same transaction, and the backend generates a
16
+ * throwaway zkApp keypair per request that is discarded before the tx is
17
+ * returned — so the target address is not observable to an attacker until
18
+ * the initialize AU inside is already signed and locked to `(vendor,
19
+ * prices)`. Combined with the in-circuit `vendor.requireEquals(empty())`
20
+ * precondition on `initialize()`, this yields a one-shot init per account.
21
+ *
22
+ * Also exports admin-side `redeployApp` / `redeployAll` for the platform
23
+ * operator to trigger post-hardfork v1→v2 migrations via the backend's
24
+ * `/admin/redeploy*` endpoints (bearer-token auth).
25
+ */
26
+
27
+ export type ProveDeployRequest = {
28
+ keeperUrl: string;
29
+ senderPublicKey: string; // vendor's fee-payer wallet
30
+ vendorAddress: string; // owner slot; may equal senderPublicKey
31
+ priceMonthlyMina: number;
32
+ priceYearlyMina: number;
33
+ priceFiveYearMina: number;
34
+ pricePerpetualMina: number;
35
+ // Per-license device cap, 0..5 (0 = binding disabled). Stored immutably in
36
+ // the contract's state at deploy; must match the cap the app is registered
37
+ // with (checked at approval).
38
+ deviceCap: number;
39
+ // Successor-generation deploys pass the announced fork activation slot so
40
+ // migrated licenses whose pre-fork expiry falls before activation get their
41
+ // expiry bumped forward at migration time (7-day grace then runs from
42
+ // activation, preserving renewal stacking). Omit or 0 for first-generation
43
+ // deploys — no ancestor tree, no bump.
44
+ forkActivationSlot?: number;
45
+ // The network to deploy on (the keeper defaults to devnet without it).
46
+ network?: 'devnet' | 'mainnet';
47
+ // The app's https:// website, written into the contract's zkappUri (the
48
+ // fixed half of the vendor's /.well-known trust anchor).
49
+ website?: string;
50
+ // Where the vendor's 98% goes (default: vendorAddress). It never signs;
51
+ // the vendor can change it later, effective after a 48 h delay.
52
+ payoutAddress?: string;
53
+ fetcher?: typeof fetch;
54
+ signal?: AbortSignal;
55
+ };
56
+
57
+ export type ProveDeployResponse = {
58
+ provenTxJson: string;
59
+ verificationKeyHash: string;
60
+ zkAppAddress: string;
61
+ };
62
+
63
+ /**
64
+ * The keeper's deploy watch (GET /deploy/status): `landed` once the zkApp
65
+ * account is on chain, `timeout` if the watch gave up waiting, `unknown` for
66
+ * an address it isn't watching.
67
+ */
68
+ export type DeployStatus = {
69
+ status: 'unknown' | 'pending' | 'landed' | 'timeout';
70
+ landedAtSlot?: number;
71
+ landedAtBlockHeight?: number;
72
+ elapsedMs?: number;
73
+ };
74
+
75
+ function baseUrl(url: string): string {
76
+ return url.replace(/\/+$/, '');
77
+ }
78
+
79
+ async function readJsonError(resp: Response): Promise<string> {
80
+ try {
81
+ const body = await resp.json() as Record<string, unknown>;
82
+ if (typeof body.error === 'string') return body.error;
83
+ } catch { /* body wasn't JSON */ }
84
+ return `HTTP ${resp.status}`;
85
+ }
86
+
87
+ // POST /prove/deploy — the backend generates the throwaway zkApp keypair,
88
+ // assembles the deploy+initialize tx, proves it, and signs the zkApp
89
+ // AccountUpdate with the throwaway key. The caller forwards `provenTxJson` to
90
+ // the vendor's wallet for the fee-payer signature.
91
+ //
92
+ // At least one of the four tier prices must be > 0 (enforced by the backend
93
+ // *and* by the circuit's `initialize()`).
94
+ export async function proveDeploy(req: ProveDeployRequest): Promise<ProveDeployResponse> {
95
+ const fetcher = req.fetcher ?? fetch;
96
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/prove/deploy`, {
97
+ method: 'POST',
98
+ headers: { 'Content-Type': 'application/json' },
99
+ signal: req.signal,
100
+ body: JSON.stringify({
101
+ senderPublicKey: req.senderPublicKey,
102
+ vendorAddress: req.vendorAddress,
103
+ priceMonthlyMina: req.priceMonthlyMina,
104
+ priceYearlyMina: req.priceYearlyMina,
105
+ priceFiveYearMina: req.priceFiveYearMina,
106
+ pricePerpetualMina: req.pricePerpetualMina,
107
+ deviceCap: req.deviceCap,
108
+ ...(req.forkActivationSlot !== undefined ? { forkActivationSlot: req.forkActivationSlot } : {}),
109
+ ...(req.network ? { network: req.network } : {}),
110
+ ...(req.website ? { website: req.website } : {}),
111
+ ...(req.payoutAddress ? { payoutAddress: req.payoutAddress } : {}),
112
+ }),
113
+ });
114
+ if (!resp.ok) throw new Error(await readJsonError(resp));
115
+ return await resp.json() as ProveDeployResponse;
116
+ }
117
+
118
+ export type PollDeployStatusOptions = {
119
+ keeperUrl: string;
120
+ zkAppAddress: string;
121
+ fetcher?: typeof fetch;
122
+ intervalMs?: number; // default 3 000
123
+ timeoutMs?: number; // default 10 min
124
+ signal?: AbortSignal;
125
+ onTick?: (status: DeployStatus) => void;
126
+ };
127
+
128
+ // Polls GET /deploy/status until it returns a terminal state ('landed' or
129
+ // 'timeout') or the timeout fires. 'unknown' means the backend hasn't seen the
130
+ // tx yet (register step not called or watch not armed).
131
+ export async function pollDeployStatus(opts: PollDeployStatusOptions): Promise<DeployStatus> {
132
+ const fetcher = opts.fetcher ?? fetch;
133
+ const interval = opts.intervalMs ?? 3_000;
134
+ const timeout = opts.timeoutMs ?? 10 * 60 * 1000;
135
+ const deadline = Date.now() + timeout;
136
+ const url = `${baseUrl(opts.keeperUrl)}/deploy/status?zkAppAddress=${encodeURIComponent(opts.zkAppAddress)}`;
137
+
138
+ while (true) {
139
+ if (opts.signal?.aborted) throw new Error('pollDeployStatus aborted');
140
+ let status: DeployStatus = { status: 'unknown' };
141
+ try {
142
+ const resp = await fetcher(url, { signal: opts.signal });
143
+ if (resp.ok) status = await resp.json() as DeployStatus;
144
+ } catch { /* transient network — try again on next tick */ }
145
+ opts.onTick?.(status);
146
+ if (status.status === 'landed' || status.status === 'timeout') return status;
147
+ if (Date.now() >= deadline) return status;
148
+ await new Promise<void>((r) => setTimeout(r, interval));
149
+ }
150
+ }
151
+
152
+ // ─── Admin redeploy ──────────────────────────────────────────────────────────
153
+ //
154
+ // Wraps the backend's `/admin/redeploy*` endpoints. These are platform-operator
155
+ // tools; the backend reads the authoritative vendor/prices from chain. The
156
+ // admin wallet (the platform key; the keeper holds no key) signs the retire
157
+ // and pays: a call answers { step: 'sign-retire', retireMessage } until
158
+ // `retireSignature` is given; with it and `feePayer` (the admin wallet's
159
+ // address) it answers the proved transaction of the app's next step, which
160
+ // the wallet signs and sends, then registerAdminRedeploy. Before a hard fork
161
+ // the step retires the app (`step: 'retire'`); after it, it deploys the
162
+ // successor, which starts with the retired app's licenses (`step: 'deploy'`).
163
+ // Guards enforced by the backend: refund window closed, escrow empty,
164
+ // on-chain version still in supported range. Auth is a platform-admin bearer
165
+ // token.
166
+
167
+ export type RedeployAppRequest = {
168
+ keeperUrl: string;
169
+ appId: string;
170
+ adminToken: string;
171
+ network?: 'devnet' | 'mainnet';
172
+ // The platform key's base58 signature over `retireMessage` from a
173
+ // sign-retire answer, made in the admin wallet (the keeper holds no key).
174
+ retireSignature?: string;
175
+ // The admin wallet's address: it signs, sends and pays for the step.
176
+ feePayer?: string;
177
+ // Retire with purchases still in escrow (their funds stay on the old
178
+ // contract for good); the platform key signs it. Last resort ahead of a fork.
179
+ force?: boolean;
180
+ fetcher?: typeof fetch;
181
+ signal?: AbortSignal;
182
+ };
183
+
184
+ /**
185
+ * A proved redeploy step for the signer's wallet to sign, send and pay
186
+ * (vendor or admin wallet); then register its hash with `redeployId`.
187
+ */
188
+ export type RedeployTx =
189
+ | { step: 'retire'; provenTxJson: string; redeployId: string; retiredAddress: string; successor: string }
190
+ | { step: 'retireAndDeploy' | 'deploy'; provenTxJson: string; redeployId: string; retiredAddress: string; zkAppAddress: string; verificationKeyHash: string };
191
+
192
+ /** The step a redeploy call took. */
193
+ export type RedeployStepResult =
194
+ | { step: 'sign-retire'; retireMessage: string[]; licensesRoot: string; successor: string; then: 'retire' | 'retireAndDeploy' | 'deploy'; force: boolean }
195
+ | RedeployTx;
196
+
197
+ export type RedeployAppResponse = { ok: true } & RedeployStepResult;
198
+
199
+ export type RedeployAllRequest = {
200
+ keeperUrl: string;
201
+ adminToken: string;
202
+ network?: 'devnet' | 'mainnet';
203
+ fetcher?: typeof fetch;
204
+ signal?: AbortSignal;
205
+ };
206
+
207
+ export type RedeployAllResponse = {
208
+ ok: true;
209
+ count: number;
210
+ /** Apps whose retirement gate is up, ready for their next step. */
211
+ ready: number;
212
+ results: Array<
213
+ | { appId: string; ok: true; step: 'retire' | 'retireAndDeploy' | 'deploy'; readyAt: number }
214
+ | { appId: string; ok: false; error: string }
215
+ >;
216
+ };
217
+
218
+ function adminHeaders(token: string): Record<string, string> {
219
+ return {
220
+ 'Content-Type': 'application/json',
221
+ 'Authorization': `Bearer ${token}`,
222
+ };
223
+ }
224
+
225
+ // POST /admin/redeploy/:appId — the next redeploy step for a single app.
226
+ // Rejects with a descriptive error if any backend guard fails (409) or the
227
+ // transaction itself throws (500). After a deploy step the returned
228
+ // `zkAppAddress` is the successor's; the old one stays on chain, retired.
229
+ export async function redeployApp(req: RedeployAppRequest): Promise<RedeployAppResponse> {
230
+ const fetcher = req.fetcher ?? fetch;
231
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/admin/redeploy/${encodeURIComponent(req.appId)}`, {
232
+ method: 'POST',
233
+ headers: adminHeaders(req.adminToken),
234
+ signal: req.signal,
235
+ body: JSON.stringify({
236
+ ...(req.network ? { network: req.network } : {}),
237
+ ...(req.retireSignature ? { retireSignature: req.retireSignature } : {}),
238
+ ...(req.feePayer ? { feePayer: req.feePayer } : {}),
239
+ ...(req.force ? { force: true } : {}),
240
+ }),
241
+ });
242
+ if (!resp.ok) throw new Error(await readJsonError(resp));
243
+ return await resp.json() as RedeployAppResponse;
244
+ }
245
+
246
+ // POST /admin/redeploy/:appId/register — the admin wallet sent the proved
247
+ // step: a retire is marked on the app; a deploy is watched until it lands,
248
+ // then the app moves to its successor.
249
+ export async function registerAdminRedeploy(req: { keeperUrl: string; appId: string; adminToken: string; redeployId: string; txHash: string; fetcher?: typeof fetch; signal?: AbortSignal }): Promise<{ ok: true; step: 'retire' | 'deploy' }> {
250
+ const resp = await (req.fetcher ?? fetch)(`${baseUrl(req.keeperUrl)}/admin/redeploy/${encodeURIComponent(req.appId)}/register`, {
251
+ method: 'POST', headers: adminHeaders(req.adminToken), signal: req.signal,
252
+ body: JSON.stringify({ redeployId: req.redeployId, txHash: req.txHash }),
253
+ });
254
+ if (!resp.ok) throw new Error(await readJsonError(resp));
255
+ return await resp.json() as { ok: true; step: 'retire' | 'deploy' };
256
+ }
257
+
258
+ // POST /admin/redeploy-all — raises every eligible app's retirement gate at
259
+ // once (one shared drain) and reports each app's next step; the admin wallet
260
+ // then takes them one app at a time with redeployApp (it signs and pays).
261
+ // Apps that can't be redeployed surface as ok:false with the reason.
262
+ export async function redeployAll(req: RedeployAllRequest): Promise<RedeployAllResponse> {
263
+ const fetcher = req.fetcher ?? fetch;
264
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/admin/redeploy-all`, {
265
+ method: 'POST',
266
+ headers: adminHeaders(req.adminToken),
267
+ signal: req.signal,
268
+ body: JSON.stringify(req.network ? { network: req.network } : {}),
269
+ });
270
+ if (!resp.ok) throw new Error(await readJsonError(resp));
271
+ return await resp.json() as RedeployAllResponse;
272
+ }
273
+
274
+ // ─── Vendor-triggered redeploy ───────────────────────────────────────────────
275
+ //
276
+ // Same steps as the admin path, but authenticated by a Mina signature against
277
+ // the on-chain vendor of the target app (no admin token required), and the
278
+ // vendor signs the retire. The vendor's wallet pays for every step (retire
279
+ // and successor). Flow, mirroring first-time /prove/deploy →
280
+ // wallet-broadcast → /apps register:
281
+ //
282
+ // 1. getVendorRedeployChallenge → { nonce, expiresAt }
283
+ // 2. vendor's wallet signs `nonce` → { field, scalar }
284
+ // 3. proveVendorRedeploy → 202 draining (come back after readyAt),
285
+ // 202 sign-retire (sign retireMessage with
286
+ // signFields, call again with
287
+ // retireSignature), then
288
+ // { step, provenTxJson, redeployId, … }
289
+ // (step 'retire' before a fork)
290
+ // 4. wallet adds fee-payer sig + broadcasts (mirrors proveDeploy flow)
291
+ // 5. registerVendorRedeploy → a retire is marked on the app; a
292
+ // deploy arms the backend's
293
+ // deploy-watcher on the new address. On
294
+ // landing, the backend promotes the new
295
+ // deployment onto the AppRecord
296
+ // (deploymentHistory[] etc).
297
+ //
298
+ // Each call to step 3 needs a fresh challenge. Guards (freeze slot
299
+ // configured, refund window closed, escrow empty, active status, supported
300
+ // version) all apply; an app retires only with an empty escrow.
301
+ //
302
+ // ⚠ Vendor-embedded buy links — after redeploy, `proveVendorRedeploy` returns
303
+ // the successor's `zkAppAddress`. Platform-hosted marketplace pages handle this
304
+ // automatically: the marketplace stores your registered `buyUrl`/`renewUrl`/
305
+ // `refundUrl` as base URLs and appends `?app=<zkAppAddress>&network=<net>`
306
+ // at render time, so the "Get" button routes to the new address the
307
+ // moment `finalizeRedeploy` promotes it onto the AppRecord — no vendor
308
+ // action required. Only vendor-hosted surfaces where the raw `zkAppAddress`
309
+ // is hand-embedded (docs, storefront, emails) need manual updating. Existing
310
+ // licenses carry on in the successor under the same hash; the retired
311
+ // address refuses new buys.
312
+
313
+ export type VendorRedeployPreflight = {
314
+ // True iff every guard passes and the redeploy can proceed.
315
+ ok: boolean;
316
+ freezeSlotConfigured: boolean;
317
+ appExists: boolean;
318
+ status?: 'active' | 'inactive' | 'pending';
319
+ refundWindow: { closed: boolean; opensAtSlot: number; currentSlot: number };
320
+ onChainAccount: { exists: boolean };
321
+ // The app retires only with an empty escrow: wait for refunds and releases.
322
+ escrow: { empty: boolean };
323
+ version: { current: number; supported: boolean; supportedList: readonly number[] };
324
+ // Live deployment already runs the keeper's current circuit.
325
+ alreadyCurrent: boolean;
326
+ // The live deployment is retired on chain: its successor is next.
327
+ retired: boolean;
328
+ // The platform is draining the app: new purchases are paused until the escrow is empty.
329
+ draining: { startedAt: string } | null;
330
+ // What the redeploy would do next (null when a guard blocks it).
331
+ step: 'retire' | 'retireAndDeploy' | 'deploy' | null;
332
+ // Set while a redeploy is in progress: the current deployment is read-only
333
+ // (no buys, renewals or device changes) until the successor lands. Before
334
+ // `readyAt` the prove call answers `draining`.
335
+ retirement: { readyAt: number; expiresAt: number } | null;
336
+ // Human-readable reasons for every failing guard, in evaluation order.
337
+ reasons: string[];
338
+ };
339
+
340
+ // GET /vendor/redeploy/:appId/preflight — read-only guard status. No nonce
341
+ // consumed, no signature required. UI decides button state from the returned
342
+ // fields; 404 means the app itself is unknown (a distinct failure mode from a
343
+ // known app failing individual guards).
344
+ export async function fetchVendorRedeployPreflight(opts: {
345
+ keeperUrl: string;
346
+ appId: string;
347
+ network: 'devnet' | 'mainnet';
348
+ fetcher?: typeof fetch;
349
+ signal?: AbortSignal;
350
+ }): Promise<VendorRedeployPreflight> {
351
+ const fetcher = opts.fetcher ?? fetch;
352
+ const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/vendor/redeploy/${encodeURIComponent(opts.appId)}/preflight?network=${opts.network}`, {
353
+ signal: opts.signal,
354
+ });
355
+ if (!resp.ok) throw new Error(await readJsonError(resp));
356
+ return await resp.json() as VendorRedeployPreflight;
357
+ }
358
+
359
+ export type VendorRedeployChallenge = {
360
+ nonce: string;
361
+ expiresAt: number; // ms epoch
362
+ };
363
+
364
+ // GET /vendor/redeploy/:appId/challenge — mint a one-shot nonce that the
365
+ // vendor's wallet signs. The nonce is bound to the appId and expires in 60s;
366
+ // consumed on /prove.
367
+ export async function getVendorRedeployChallenge(opts: {
368
+ keeperUrl: string;
369
+ appId: string;
370
+ /** The deployment to redeploy (the keeper requires it). */
371
+ network: 'devnet' | 'mainnet';
372
+ fetcher?: typeof fetch;
373
+ signal?: AbortSignal;
374
+ }): Promise<VendorRedeployChallenge> {
375
+ const fetcher = opts.fetcher ?? fetch;
376
+ const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/vendor/redeploy/${encodeURIComponent(opts.appId)}/challenge?network=${opts.network}`, {
377
+ signal: opts.signal,
378
+ });
379
+ if (!resp.ok) throw new Error(await readJsonError(resp));
380
+ return await resp.json() as VendorRedeployChallenge;
381
+ }
382
+
383
+ export type ProveVendorRedeployRequest = {
384
+ keeperUrl: string;
385
+ appId: string;
386
+ nonce: string;
387
+ signature: { field: string; scalar: string };
388
+ senderPublicKey: string; // vendor's fee-payer wallet
389
+ // The vendor's base58 signature over `retireMessage` (from the sign-retire
390
+ // answer), made with the wallet's signFields.
391
+ retireSignature?: string;
392
+ fetcher?: typeof fetch;
393
+ signal?: AbortSignal;
394
+ };
395
+
396
+ // The successor's zkApp address (on a deploy step) is where the app moves.
397
+ // Marketplace links auto-update: the platform appends
398
+ // `?app=<zkAppAddress>&network=<net>` to your registered `buyUrl`/`renewUrl`/
399
+ // `refundUrl` at render time, so the "Get" button routes to this new address
400
+ // once `finalizeRedeploy` promotes it onto the AppRecord — no vendor action
401
+ // required. Only vendor-hosted surfaces where the raw address is embedded by
402
+ // hand need manual updating. `redeployId` is the capability token for
403
+ // registerVendorRedeploy.
404
+ export type ProveVendorRedeployResponse = RedeployTx;
405
+
406
+ // Returned (HTTP 202) by the first prove call: the keeper has made the current
407
+ // deployment read-only and is waiting out in-flight proofs before the app can
408
+ // retire. Call proveVendorRedeploy again (fresh challenge + signature) after
409
+ // `readyAt`, before `expiresAt`.
410
+ export type VendorRedeployDraining = {
411
+ status: 'draining';
412
+ readyAt: number; // epoch ms
413
+ expiresAt: number; // epoch ms — the pause lifts if the redeploy isn't finished
414
+ message: string;
415
+ };
416
+
417
+ // Returned (HTTP 202) when the app must retire and no retireSignature was
418
+ // given: sign `retireMessage` (field strings) with the vendor key and call
419
+ // again with `retireSignature`.
420
+ export type VendorRedeploySignRetire = {
421
+ status: 'sign-retire';
422
+ retireMessage: string[];
423
+ licensesRoot: string;
424
+ /** The new zkApp address the retirement names: only it can start from these licenses. */
425
+ successor: string;
426
+ message: string;
427
+ };
428
+
429
+ // POST /vendor/redeploy/:appId/prove — the backend verifies the nonce +
430
+ // signature against the on-chain vendor of the app and runs all redeploy
431
+ // guards, then answers with the next step (see the flow above).
432
+ export async function proveVendorRedeploy(req: ProveVendorRedeployRequest): Promise<
433
+ ProveVendorRedeployResponse | VendorRedeployDraining | VendorRedeploySignRetire
434
+ > {
435
+ const fetcher = req.fetcher ?? fetch;
436
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/vendor/redeploy/${encodeURIComponent(req.appId)}/prove`, {
437
+ method: 'POST',
438
+ headers: { 'Content-Type': 'application/json' },
439
+ signal: req.signal,
440
+ body: JSON.stringify({
441
+ nonce: req.nonce,
442
+ signature: req.signature,
443
+ senderPublicKey: req.senderPublicKey,
444
+ ...(req.retireSignature ? { retireSignature: req.retireSignature } : {}),
445
+ }),
446
+ });
447
+ if (!resp.ok) throw new Error(await readJsonError(resp));
448
+ return await resp.json() as ProveVendorRedeployResponse | VendorRedeployDraining | VendorRedeploySignRetire;
449
+ }
450
+
451
+ export type RegisterVendorRedeployRequest = {
452
+ keeperUrl: string;
453
+ appId: string;
454
+ redeployId: string; // capability token from proveVendorRedeploy
455
+ txHash: string; // returned by the vendor's wallet after broadcast
456
+ fetcher?: typeof fetch;
457
+ signal?: AbortSignal;
458
+ };
459
+
460
+ // POST /vendor/redeploy/:appId/register — arm the backend's deploy-watcher on
461
+ // the new address. On confirmation the backend mutates AppRecord to promote
462
+ // the new deployment (push old to deploymentHistory[], overwrite live fields,
463
+ // clear event-sync cursors). Idempotent to duplicate calls (the watcher
464
+ // checks whether one is already armed for the address). Poll GET
465
+ // /deploy/status?zkAppAddress=<new> — or use pollDeployStatus — for landing
466
+ // progress, same as first-time deploys.
467
+ // POST /vendor/redeploy/:appId/cancel — calls off the vendor's redeploy
468
+ // (gate, drain) before the app is retired on chain. A fresh signed challenge.
469
+ export async function cancelVendorRedeploy(req: { keeperUrl: string; appId: string; nonce: string; signature: { field: string; scalar: string }; fetcher?: typeof fetch; signal?: AbortSignal }): Promise<{ ok: true }> {
470
+ const fetcher = req.fetcher ?? fetch;
471
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/vendor/redeploy/${encodeURIComponent(req.appId)}/cancel`, {
472
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: req.signal,
473
+ body: JSON.stringify({ nonce: req.nonce, signature: req.signature }),
474
+ });
475
+ if (!resp.ok) throw new Error(await readJsonError(resp));
476
+ return await resp.json() as { ok: true };
477
+ }
478
+
479
+ // POST /admin/redeploy/:appId/drain — the platform starts a redeploy of an
480
+ // app with purchases in escrow: new purchases pause until it is empty.
481
+ export async function startRedeployDrain(req: { keeperUrl: string; appId: string; adminToken: string; network: 'devnet' | 'mainnet'; fetcher?: typeof fetch }): Promise<{ ok: true; draining: { startedAt: string }; alreadyEmpty: boolean }> {
482
+ const resp = await (req.fetcher ?? fetch)(`${baseUrl(req.keeperUrl)}/admin/redeploy/${encodeURIComponent(req.appId)}/drain`, {
483
+ method: 'POST', headers: adminHeaders(req.adminToken), body: JSON.stringify({ network: req.network }),
484
+ });
485
+ if (!resp.ok) throw new Error(await readJsonError(resp));
486
+ return await resp.json() as { ok: true; draining: { startedAt: string }; alreadyEmpty: boolean };
487
+ }
488
+
489
+ // POST /admin/redeploy/:appId/cancel — calls off a redeploy or drain not yet retired on chain.
490
+ export async function cancelRedeploy(req: { keeperUrl: string; appId: string; adminToken: string; network: 'devnet' | 'mainnet'; fetcher?: typeof fetch }): Promise<{ ok: true }> {
491
+ const resp = await (req.fetcher ?? fetch)(`${baseUrl(req.keeperUrl)}/admin/redeploy/${encodeURIComponent(req.appId)}/cancel`, {
492
+ method: 'POST', headers: adminHeaders(req.adminToken), body: JSON.stringify({ network: req.network }),
493
+ });
494
+ if (!resp.ok) throw new Error(await readJsonError(resp));
495
+ return await resp.json() as { ok: true };
496
+ }
497
+
498
+ // POST /admin/redeploy-notice — ahead of an announced hard fork, emails the vendors of live apps.
499
+ export async function sendRedeployNotices(req: { keeperUrl: string; adminToken: string; network: 'devnet' | 'mainnet'; fetcher?: typeof fetch }): Promise<{ ok: true; notified: string[]; forkActivationSlot: number }> {
500
+ const resp = await (req.fetcher ?? fetch)(`${baseUrl(req.keeperUrl)}/admin/redeploy-notice`, {
501
+ method: 'POST', headers: adminHeaders(req.adminToken), body: JSON.stringify({ network: req.network }),
502
+ });
503
+ if (!resp.ok) throw new Error(await readJsonError(resp));
504
+ return await resp.json() as { ok: true; notified: string[]; forkActivationSlot: number };
505
+ }
506
+
507
+ export async function registerVendorRedeploy(req: RegisterVendorRedeployRequest): Promise<{ ok: true; step: 'retire' | 'deploy' }> {
508
+ const fetcher = req.fetcher ?? fetch;
509
+ const resp = await fetcher(`${baseUrl(req.keeperUrl)}/vendor/redeploy/${encodeURIComponent(req.appId)}/register`, {
510
+ method: 'POST',
511
+ headers: { 'Content-Type': 'application/json' },
512
+ signal: req.signal,
513
+ body: JSON.stringify({ redeployId: req.redeployId, txHash: req.txHash }),
514
+ });
515
+ if (!resp.ok) throw new Error(await readJsonError(resp));
516
+ return await resp.json() as { ok: true; step: 'retire' | 'deploy' };
517
+ }