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,221 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * licenseLeaf.ts
6
+ *
7
+ * On-chain leaf value stored at every occupied `licensesRoot` slot. Bundles
8
+ * the license expiry with the device-binding state (cap, slot commitments,
9
+ * fill counter) so a single tree lookup returns everything a verifier or a
10
+ * `/prove/bind` call needs.
11
+ *
12
+ * Wire shape (in order of hash preimage):
13
+ * licenseHash Field the tree key, also carried in the leaf
14
+ * so a stolen witness can't be re-pointed
15
+ * at another entry. Doubles as the owner-
16
+ * pubkey commitment — buyLicense asserts
17
+ * licenseHash == Poseidon(pk.x, pk.isOdd)
18
+ * so bind/unbind's ownership check reads
19
+ * the same field as the tree key. Storing
20
+ * them separately would be pure redundancy.
21
+ * expirySlot UInt32→Field UInt32 packed into a Field
22
+ * cap UInt32→Field 0 disables /prove/bind; ≤DEVICE_SLOT_CAP
23
+ * (kept as UInt32 in-circuit because o1js's
24
+ * provable types don't include UInt8)
25
+ * deviceSlotsRoot Field Poseidon fold over the 10 slot fields
26
+ * below (2 fields × DEVICE_SLOT_CAP)
27
+ * deviceSlotsFilled UInt32→Field how many of the DEVICE_SLOT_CAP slots
28
+ * are currently non-empty — carried
29
+ * alongside the fold so `bind()` can do
30
+ * a cheap "cap - filled > 0" check
31
+ * without re-summing the array
32
+ *
33
+ * DEVICE_SLOT_CAP is fixed at 5 so the slot arrays have a size known to the
34
+ * circuit at compile time. `cap` in the leaf may be smaller — it's a
35
+ * per-license limit; slots [cap..DEVICE_SLOT_CAP) must always stay empty
36
+ * (Field(0)) and `bind()` rejects any write to them. `cap = 0` disables
37
+ * binding entirely; `bind()` rejects immediately without ever touching a
38
+ * slot.
39
+ *
40
+ * Slot encoding (fields carried in the fold, per slot i):
41
+ * occupied[i] Field 0 (empty) or Poseidon(devicePubKeyHash)
42
+ * freedAt[i] Field packed (boundAtSlot << 32) | freedAtSlot; both
43
+ * are UInt32-range Mina slot numbers. On a fresh
44
+ * empty slot both halves are 0. On bind, boundAt
45
+ * gets the current slot and freedAt stays 0. On
46
+ * unbind, freedAt gets the current slot (boundAt
47
+ * is preserved for audit / cooldown math).
48
+ *
49
+ * The struct + helpers live in the SDK (not the circuit package) so both
50
+ * the on-chain `@method` bodies AND the off-chain backend / verifier /
51
+ * dashboards can share a single source of truth for the leaf hash. The
52
+ * circuit re-exports them from `LicensingApp.ts` so existing consumers
53
+ * that import from there keep working.
54
+ */
55
+
56
+ import { Field, Poseidon, Provable, PublicKey, Struct, UInt32 } from 'o1js';
57
+ import type { LicenseRecord } from './licenseRecord.js';
58
+
59
+ // Fixed slot-array width. Per the device-binding-noncustodial plan: 5 is
60
+ // the sweet spot between "enough seats for a normal user" and "witnesses
61
+ // stay small." Changing this is a hardfork migration (would flip every
62
+ // leaf hash) — not a runtime tunable.
63
+ export const DEVICE_SLOT_CAP_N = 5;
64
+ export const DEVICE_SLOT_CAP = UInt32.from(DEVICE_SLOT_CAP_N);
65
+
66
+ // Poseidon-fold of DEVICE_SLOT_CAP × 2 = 10 zero Fields, used as the
67
+ // initial deviceSlotsRoot on every freshly-minted leaf. Precomputed once
68
+ // at module load so `freshLeaf()` doesn't reconstruct it per-call.
69
+ export const EMPTY_DEVICE_SLOTS_ROOT: Field = Poseidon.hash(
70
+ Array.from({ length: DEVICE_SLOT_CAP_N * 2 }, () => Field(0)),
71
+ );
72
+
73
+ // Poseidon-bound identity of the passphrase-derived ownership pubkey.
74
+ // The full PublicKey is (x: Field, isOdd: Bool); Poseidon(x, isOdd.toField())
75
+ // gives a single Field commitment. The buy circuit asserts this equals
76
+ // `licenseHash`, so the leaf carries no separate owner-pubkey field —
77
+ // `licenseHash` IS the on-chain owner-pubkey commitment.
78
+ export function ownerPubKeyHash(pub: PublicKey): Field {
79
+ return Poseidon.hash([pub.x, pub.isOdd.toField()]);
80
+ }
81
+
82
+ // Pack (boundAtSlot, freedAtSlot) into a single Field. Both are Mina
83
+ // slot numbers (UInt32-range, ≤ 2^32-1), so the concatenation fits in 64
84
+ // bits. Layout mirrors packPrices in LicensingApp.ts — low bits first so
85
+ // the more frequently-read half (freedAt, checked by every bind cooldown)
86
+ // unpacks with a single mask.
87
+ const SHIFT_32 = Field(1n << 32n);
88
+
89
+ export function packSlotTimes(boundAtSlot: UInt32, freedAtSlot: UInt32): Field {
90
+ return freedAtSlot.value.add(boundAtSlot.value.mul(SHIFT_32));
91
+ }
92
+
93
+ export function unpackSlotTimes(packed: Field): { boundAtSlot: UInt32; freedAtSlot: UInt32 } {
94
+ // toBits(64) enforces the high 190 bits are zero — belt-and-suspenders
95
+ // since we only ever write via packSlotTimes, but keeps the circuit
96
+ // sound against an adversarially-crafted slot in a future upgrade.
97
+ const bits = packed.toBits(64);
98
+ const freedAtSlot = UInt32.Unsafe.fromField(Field.fromBits(bits.slice(0, 32)));
99
+ const boundAtSlot = UInt32.Unsafe.fromField(Field.fromBits(bits.slice(32, 64)));
100
+ return { boundAtSlot, freedAtSlot };
101
+ }
102
+
103
+ // Fold the parallel slot arrays into `deviceSlotsRoot`. Same order every
104
+ // time — occupied[0], freedAt[0], occupied[1], freedAt[1], ... — so a
105
+ // buyer that reconstructs the fold off-chain (verifier, dashboard) gets
106
+ // the same commitment the circuit computed.
107
+ export function computeDeviceSlotsRoot(
108
+ occupied: Field[],
109
+ freedAt: Field[],
110
+ ): Field {
111
+ if (occupied.length !== DEVICE_SLOT_CAP_N || freedAt.length !== DEVICE_SLOT_CAP_N) {
112
+ throw new Error(
113
+ `computeDeviceSlotsRoot expects exactly ${DEVICE_SLOT_CAP_N} entries in each array ` +
114
+ `(got occupied=${occupied.length}, freedAt=${freedAt.length})`,
115
+ );
116
+ }
117
+ const fields: Field[] = [];
118
+ for (let i = 0; i < DEVICE_SLOT_CAP_N; i++) {
119
+ fields.push(occupied[i], freedAt[i]);
120
+ }
121
+ return Poseidon.hash(fields);
122
+ }
123
+
124
+ // Per-slot payload. The Struct wrapper is what lets o1js serialize this
125
+ // inside larger provable structures — it also gives us a fixed
126
+ // two-Field width so DeviceSlots.slots can be a `Provable.Array` of a
127
+ // well-known type instead of a bag of loose Fields.
128
+ //
129
+ // Same `const X: any = Struct(...)` pattern as contractInterface.ts —
130
+ // annotating as `any` sidesteps TS2742 (o1js sits outside the package
131
+ // rootDir under npm hoisting, so tsc can't emit portable declarations
132
+ // for the inferred Struct class). Instance methods and `static empty()`
133
+ // therefore live as standalone helpers below rather than on the class.
134
+ export const DeviceSlot: any = Struct({
135
+ occupied: Field, // 0 or Poseidon(devicePubKeyHash)
136
+ freedAt: Field, // packSlotTimes(boundAtSlot, freedAtSlot)
137
+ });
138
+ export type DeviceSlot = any;
139
+
140
+ export function emptyDeviceSlot(): DeviceSlot {
141
+ return new DeviceSlot({ occupied: Field(0), freedAt: Field(0) });
142
+ }
143
+
144
+ // Fixed-width slot array. `Provable.Array(DeviceSlot, N)` promises the
145
+ // circuit an exact length so witnesses can address slots by static index.
146
+ export const DeviceSlots: any = Struct({
147
+ slots: Provable.Array(DeviceSlot, DEVICE_SLOT_CAP_N),
148
+ });
149
+ export type DeviceSlots = any;
150
+
151
+ export function emptyDeviceSlots(): DeviceSlots {
152
+ return new DeviceSlots({
153
+ slots: Array.from({ length: DEVICE_SLOT_CAP_N }, () => emptyDeviceSlot()),
154
+ });
155
+ }
156
+
157
+ // In-circuit fold — takes a DeviceSlots value so it composes cleanly
158
+ // inside `@method` bodies. The array length is fixed by the Struct's
159
+ // Provable.Array declaration, so no runtime length guard is needed.
160
+ export function deviceSlotsHash(ds: DeviceSlots): Field {
161
+ const fields: Field[] = [];
162
+ for (const s of ds.slots) {
163
+ fields.push(s.occupied, s.freedAt);
164
+ }
165
+ return Poseidon.hash(fields);
166
+ }
167
+
168
+ // The leaf value itself. `LicensingApp.buyLicense` builds one of these
169
+ // with EMPTY_DEVICE_SLOTS_ROOT and writes leafHash(leaf) into licensesRoot;
170
+ // subsequent mutations (renew, refund, migrate, bind, unbind) each build
171
+ // a new leaf with the appropriate field(s) updated and write its hash.
172
+ export const LicenseLeaf: any = Struct({
173
+ licenseHash: Field,
174
+ expirySlot: UInt32,
175
+ cap: UInt32,
176
+ deviceSlotsRoot: Field,
177
+ deviceSlotsFilled: UInt32,
178
+ });
179
+ export type LicenseLeaf = any;
180
+
181
+ export function leafHash(leaf: LicenseLeaf): Field {
182
+ return Poseidon.hash([
183
+ leaf.licenseHash,
184
+ leaf.expirySlot.value,
185
+ leaf.cap.value,
186
+ leaf.deviceSlotsRoot,
187
+ leaf.deviceSlotsFilled.value,
188
+ ]);
189
+ }
190
+
191
+ // Construct the leaf a fresh buy writes into the tree — no devices bound
192
+ // yet, filled counter at zero, slot-fold at the precomputed empty root.
193
+ export function freshLeaf(
194
+ licenseHash: Field,
195
+ expirySlot: UInt32,
196
+ cap: UInt32,
197
+ ): LicenseLeaf {
198
+ return new LicenseLeaf({
199
+ licenseHash,
200
+ expirySlot,
201
+ cap,
202
+ deviceSlotsRoot: EMPTY_DEVICE_SLOTS_ROOT,
203
+ deviceSlotsFilled: UInt32.zero,
204
+ });
205
+ }
206
+
207
+ // Reconstruct the leaf that lives at `record.licenseHash` in the licensesRoot
208
+ // MerkleMap. `LicenseRecord` optionally persists the device-binding fields;
209
+ // anything missing falls back to the fresh-leaf defaults, matching the state
210
+ // a just-issued license was minted with.
211
+ export function leafFromRecord(record: LicenseRecord): LicenseLeaf {
212
+ return new LicenseLeaf({
213
+ licenseHash: Field(record.licenseHash),
214
+ expirySlot: UInt32.from(record.expirySlot),
215
+ cap: UInt32.from(record.deviceCap ?? 0),
216
+ deviceSlotsRoot: record.deviceSlotsRoot !== undefined
217
+ ? Field(record.deviceSlotsRoot)
218
+ : EMPTY_DEVICE_SLOTS_ROOT,
219
+ deviceSlotsFilled: UInt32.from(record.deviceSlotsFilled ?? 0),
220
+ });
221
+ }
@@ -0,0 +1,190 @@
1
+ // Copyright (c) 2025-2026 zkLicensing project developers
2
+ // SPDX-License-Identifier: BUSL-1.1
3
+
4
+ /**
5
+ * licenseRecord.ts
6
+ *
7
+ * The `LicenseRecord` shape and related pure types/constants. Split out from
8
+ * licenseStore.ts (which pulls in Node `fs`) so browser-side modules — the
9
+ * buy client, the stats client, the country-stats computation — can consume
10
+ * the same type without dragging in filesystem code. licenseStore.ts
11
+ * re-exports these to keep existing Node imports working.
12
+ */
13
+
14
+ // MINA→fiat rate snapshot captured at the moment a business event happens
15
+ // (registration, purchase, renewal). Immutable — the value at time of event
16
+ // is what tax authorities want, so the backend snapshots against the same
17
+ // 5-minute CoinGecko cache used by receipts and never re-fetches. Records
18
+ // created before the very first successful CoinGecko response get no
19
+ // snapshot; dashboards render fiat as `—` for those.
20
+ export type FiatRateSnapshot = {
21
+ minaEurRate: number;
22
+ minaUsdRate: number;
23
+ minaGbpRate: number;
24
+ recordedAt: string;
25
+ };
26
+
27
+ export type LicenseRecord = {
28
+ /** Carried over from this earlier deployment when the app was redeployed (its successor starts with the same licenses): license state only, the sale stays on the earlier record. */
29
+ carriedOverFrom?: string;
30
+ /**
31
+ * Moved to another owner key (transferLicense): this record keeps the sale,
32
+ * and the license itself is the record under `transferredTo` (which names
33
+ * its origin in `transferredFrom`, with no sale of its own).
34
+ */
35
+ transferredTo?: string;
36
+ transferredFrom?: string;
37
+ zkAppAddress: string; // REQUIRED — composite key with licenseHash
38
+ licenseHash: string; // Poseidon([pk.x, pk.isOdd.toField()]) over the buyer's passphrase-derived Pallas Schnorr pubkey — see ownershipLicenseHash.ts
39
+ expirySlot: number; // 0 = refunded / empty
40
+ purchaseSlot: number; // block slot when purchased (0 after escrow released/refunded)
41
+ purchaseAmount: string; // nanomina paid (as string to avoid BigInt serialisation issues)
42
+ txHash: string;
43
+ createdAt: string;
44
+ // The three fields below reconstruct the on-chain LicenseLeaf so the backend
45
+ // can rebuild licensesRoot and produce witnesses for renew/refund/bind.
46
+ // (The leaf's owner-pubkey commitment lives in `licenseHash` itself —
47
+ // buyLicense asserts licenseHash == Poseidon(pk.x, pk.isOdd) — so there is
48
+ // no separate ownerPubKeyHash field.)
49
+ deviceCap?: number; // per-license device cap (0 = binding disabled)
50
+ deviceSlotsRoot?: string; // Poseidon fold over the fixed-width slot array
51
+ deviceSlotsFilled?: number; // count of currently-occupied slots
52
+ // Populated by eventSyncLoop from DeviceBound/DeviceUnbound events — array
53
+ // position IS slotIndex, absent entries or entries with `deviceCommitment === '0'`
54
+ // are empty. Source of truth is on-chain; this mirror is for the API surface.
55
+ devices?: Array<{
56
+ slotIndex: number;
57
+ deviceCommitment: string; // "0" = empty slot; otherwise Poseidon(devicePubKeyHash)
58
+ boundAtSlot: number; // 0 for empty
59
+ freedAtSlot: number; // 0 for pristine; the slot when last unbind fired
60
+ }>;
61
+ buyerAddress: string; // buyer's public key (B62q...) — the payer of the buy tx. Required so buyers can enumerate their licenses across origins via GET /licenses?buyer=<addr>, and to reconstruct the escrow commitment for auto-release. Legacy records lacking this field will simply not enumerate — no backfill.
62
+ // buyerCountry / buyerZip are NOT persisted to licenses.json by the backend —
63
+ // they live in the backend's append-only buyer-attribution.log and get
64
+ // joined into records at read time via enrichRecordsWithAttribution().
65
+ // Kept optional on the type so backend endpoints that echo enriched records
66
+ // (e.g. /stats, /transactions) can still expose them; browser-side records
67
+ // materialized purely from disk (no backend enrichment step) will never
68
+ // carry them.
69
+ buyerCountry?: string; // ISO 3166-1 alpha-2 self-declared buyer jurisdiction
70
+ buyerZip?: string; // US-only postal code (state-level sales-tax attribution)
71
+ // VAT evidence, joined in the same way: the country of the buyer's IP
72
+ // address at purchase (null: unknown or private address) and the
73
+ // geolocation database version. The address itself is never kept.
74
+ buyerIpCountry?: string | null;
75
+ geoDb?: string;
76
+ pending?: boolean; // true if staged at prove-time but not yet confirmed by chain events
77
+ provedAtSlot?: number; // slot at which prove succeeded — used by reaper to drop unsubmitted records
78
+ // The record as it was before this pending change (a renewal or refund
79
+ // staged over it), restored if the transaction fails on chain or never
80
+ // lands. Absent on a pending purchase: the record is removed instead.
81
+ pendingPrior?: LicenseRecord;
82
+ // Captured at licenseIssued and never mutated by renewal. Lets stats decide
83
+ // who's past their first paid period without re-walking events.
84
+ initialExpirySlot?: number;
85
+ // Back-derived from (initialExpirySlot - purchaseSlot) at issue time. Lets
86
+ // licensesByTier roll up active counts without keeping the issued slot.
87
+ tier?: 'monthly' | 'yearly' | 'fiveYear' | 'perpetual';
88
+ // True after a licenseRenewed event has been observed. Lets the stats
89
+ // accumulator detect the FIRST renewal per license without re-counting.
90
+ renewedAtLeastOnce?: boolean;
91
+ // Refund state.
92
+ // undefined | false → not refunded
93
+ // 'fast' → the tx-land watcher observed a refund tx in bestchain
94
+ // but the archive event-sync hasn't confirmed it yet.
95
+ // Treated as refunded for filtering (truthy), but a
96
+ // reorg-revert reaper will roll it back to false if
97
+ // the event never appears in the archive within
98
+ // REORG_REVERT_AFTER_SLOTS.
99
+ // true → confirmed by a canonical refundIssued event in the
100
+ // archive (or written directly by the pre-provisional
101
+ // code path).
102
+ refunded?: boolean | 'fast';
103
+ // Release state. Same two-tier semantics as `refunded` above — see
104
+ // recordRelease / revertProvisionalFlip in licenseStore.ts.
105
+ released?: boolean | 'fast';
106
+ releaseSlot?: number;
107
+ // Tx hash of the fundsReleased broadcast. Persisted so the vendor's
108
+ // transaction feed can link the release row to the explorer. Optional for
109
+ // backward-compat: pre-tracking records and archive-replayed events
110
+ // (which don't carry the tx hash) simply omit it.
111
+ releaseTxHash?: string;
112
+ // Each renewal pushes { slot, amount, newExpiry, txHash?, fiatSnapshot? }
113
+ // where amount is the nanomina paid. newExpiry is the resulting expiry —
114
+ // used as a dedup key so confirm-time entries don't double up when archive
115
+ // sync later replays the same renewedEvent. fiatSnapshot captures the
116
+ // MINA→fiat rates at renewal confirm time (missing on archive-replayed
117
+ // renewals, which have no live backend context).
118
+ renewals?: Array<{ slot: number; amount: string; newExpiry: number; txHash?: string; fiatSnapshot?: FiatRateSnapshot; waiverGivenBy?: AgentWaiverRecord; ipCountry?: string | null; geoDb?: string }>;
119
+ // MINA→fiat rates captured at buy confirm time. Immutable — the rate at
120
+ // time of purchase is what tax authorities want. Missing on records created
121
+ // before the first successful CoinGecko response and on archive-replayed
122
+ // purchases (dashboards render fiat as `—`).
123
+ fiatSnapshot?: FiatRateSnapshot;
124
+ // ISO timestamp of the buyer's express waiver of the 14-day refund window,
125
+ // captured at buy time when a FREEZE_AFTER_SLOT was scheduled on the
126
+ // backend. Absent for normal (non-freeze-window) buys. Kept for audit /
127
+ // dispute purposes — the waiver is the buyer's acknowledgment that the
128
+ // scheduled hardfork can truncate their refund window.
129
+ refundWaiverAcceptedAt?: string;
130
+ // True when the buy was minted through the no-escrow circuit method
131
+ // (buyLicenseNoEscrow). Funds went straight to vendor + platform (98/2)
132
+ // rather than into the contract's escrow tree, so there is nothing to
133
+ // refund. The buyer accepted this at purchase (see refundWaiverAcceptedAt).
134
+ // The UI uses this flag to hide the "Refund" CTA on the licenses page.
135
+ escrowless?: boolean;
136
+ // Set when the platform force-retired the app (Vendor Terms §8.7) while
137
+ // this purchase was still in escrow: its funds stay on the retired
138
+ // contract, which settles nothing any more, so no refund or release can
139
+ // follow. The license itself carried over to the successor.
140
+ escrowStranded?: { txHash: string; at: string };
141
+ // Set when the buyer's withdrawal waiver was given by an AI agent under a
142
+ // standing consent the buyer signed (Terms §7.8), so the vendor can see it.
143
+ waiverGivenBy?: AgentWaiverRecord;
144
+ // Slot at which released|refunded was set to 'fast' by the tx-land watcher.
145
+ // The reorg-revert reaper compares against currentSlot to decide when to
146
+ // give up waiting for archive confirmation and roll a 'fast' flip back to
147
+ // false. Absent when neither field is provisional.
148
+ provisionalFlipAtSlot?: number;
149
+ };
150
+
151
+ // Reap pending records this many slots after prove time if no event ever lands.
152
+ // 60 slots ≈ 90 minutes — a generous window for tx inclusion under devnet conditions.
153
+ export const PENDING_REAP_SLOTS = 60;
154
+
155
+ export type LicenseStatus = 'active' | 'refunded' | 'expired' | 'transferred' | 'unknown';
156
+
157
+ /** A withdrawal waiver an agent gave under the buyer's standing consent. */
158
+ export type AgentWaiverRecord = {
159
+ agent: string;
160
+ owner: string;
161
+ /** SHA-256 of the signed statement; the full consent is in the keeper's consent log. */
162
+ statementSha256: string;
163
+ issuedAt: string;
164
+ };
165
+
166
+ /**
167
+ * The file name of the license receipt a buyer downloads: `license-<appId>.json`,
168
+ * so an app looks for its own file and a buyer's receipts for different apps
169
+ * don't collide. Falls back to the zkApp address when there is no app ID (a
170
+ * contract that isn't registered as an app).
171
+ */
172
+ export function licenseFileName(appId: string | null | undefined, zkAppAddress?: string): string {
173
+ if (appId && /^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$/.test(appId)) return `license-${appId}.json`;
174
+ if (zkAppAddress && /^B62q[1-9A-HJ-NP-Za-km-z]{51}$/.test(zkAppAddress)) return `license-${zkAppAddress}.json`;
175
+ return 'license.json';
176
+ }
177
+
178
+ /**
179
+ * The 98/2 split exactly as the licensing circuit pays it (LicensingApp:
180
+ * fee = amount.div(50), vendor = amount.sub(fee)); the two always add up to
181
+ * the amount. Use these for every off-chain figure, so dashboards, stats
182
+ * and tax exports match the on-chain payouts to the nanomina.
183
+ */
184
+ export function platformFeeNano(amount: bigint): bigint {
185
+ return amount / 50n;
186
+ }
187
+
188
+ export function vendorShareNano(amount: bigint): bigint {
189
+ return amount - amount / 50n;
190
+ }