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.
- package/README.md +417 -201
- package/build/agent.d.ts +371 -0
- package/build/agent.d.ts.map +1 -0
- package/build/agent.js +553 -0
- package/build/agent.js.map +1 -0
- package/build/agentActions.d.ts +841 -0
- package/build/agentActions.d.ts.map +1 -0
- package/build/agentActions.js +581 -0
- package/build/agentActions.js.map +1 -0
- package/build/agentRecord.d.ts +83 -0
- package/build/agentRecord.d.ts.map +1 -0
- package/build/agentRecord.js +69 -0
- package/build/agentRecord.js.map +1 -0
- package/build/agentVendor.d.ts +220 -0
- package/build/agentVendor.d.ts.map +1 -0
- package/build/agentVendor.js +306 -0
- package/build/agentVendor.js.map +1 -0
- package/build/appsLookup.d.ts +14 -0
- package/build/appsLookup.d.ts.map +1 -0
- package/build/appsLookup.js +12 -0
- package/build/appsLookup.js.map +1 -0
- package/build/archiveBootstrap.d.ts +31 -5
- package/build/archiveBootstrap.d.ts.map +1 -0
- package/build/archiveBootstrap.js +140 -47
- package/build/archiveBootstrap.js.map +1 -0
- package/build/atomicJson.d.ts +5 -0
- package/build/atomicJson.d.ts.map +1 -0
- package/build/atomicJson.js +55 -0
- package/build/atomicJson.js.map +1 -0
- package/build/bindClient.d.ts +85 -0
- package/build/bindClient.d.ts.map +1 -0
- package/build/bindClient.js +187 -0
- package/build/bindClient.js.map +1 -0
- package/build/buyClient.d.ts +27 -4
- package/build/buyClient.d.ts.map +1 -0
- package/build/buyClient.js +72 -26
- package/build/buyClient.js.map +1 -0
- package/build/chainHead.d.ts +61 -0
- package/build/chainHead.d.ts.map +1 -0
- package/build/chainHead.js +88 -0
- package/build/chainHead.js.map +1 -0
- package/build/compactMerkleMap.d.ts +72 -0
- package/build/compactMerkleMap.d.ts.map +1 -0
- package/build/compactMerkleMap.js +224 -0
- package/build/compactMerkleMap.js.map +1 -0
- package/build/contractInterface.d.ts +191 -51
- package/build/contractInterface.d.ts.map +1 -0
- package/build/contractInterface.js +234 -43
- package/build/contractInterface.js.map +1 -0
- package/build/contractTypes.d.ts +9 -0
- package/build/contractTypes.d.ts.map +1 -0
- package/build/contractTypes.js +28 -0
- package/build/contractTypes.js.map +1 -0
- package/build/countryStats.d.ts +12 -3
- package/build/countryStats.d.ts.map +1 -0
- package/build/countryStats.js +87 -23
- package/build/countryStats.js.map +1 -0
- package/build/deployClient.d.ts +138 -24
- package/build/deployClient.d.ts.map +1 -0
- package/build/deployClient.js +91 -31
- package/build/deployClient.js.map +1 -0
- package/build/deviceIdentity.d.ts +68 -0
- package/build/deviceIdentity.d.ts.map +1 -0
- package/build/deviceIdentity.js +209 -0
- package/build/deviceIdentity.js.map +1 -0
- package/build/freezeClient.d.ts +26 -6
- package/build/freezeClient.d.ts.map +1 -0
- package/build/freezeClient.js +30 -6
- package/build/freezeClient.js.map +1 -0
- package/build/generationClient.d.ts +6 -3
- package/build/generationClient.d.ts.map +1 -0
- package/build/generationClient.js +25 -38
- package/build/generationClient.js.map +1 -0
- package/build/holderSecret.d.ts +70 -0
- package/build/holderSecret.d.ts.map +1 -0
- package/build/holderSecret.js +76 -0
- package/build/holderSecret.js.map +1 -0
- package/build/index.d.ts +38 -38
- package/build/index.d.ts.map +1 -0
- package/build/index.js +97 -121
- package/build/index.js.map +1 -0
- package/build/keysClient.d.ts +142 -0
- package/build/keysClient.d.ts.map +1 -0
- package/build/keysClient.js +109 -0
- package/build/keysClient.js.map +1 -0
- package/build/licenseLeaf.d.ts +76 -0
- package/build/licenseLeaf.d.ts.map +1 -0
- package/build/licenseLeaf.js +189 -0
- package/build/licenseLeaf.js.map +1 -0
- package/build/licenseRecord.d.ts +68 -5
- package/build/licenseRecord.d.ts.map +1 -0
- package/build/licenseRecord.js +26 -1
- package/build/licenseRecord.js.map +1 -0
- package/build/licenseStore.d.ts +81 -4
- package/build/licenseStore.d.ts.map +1 -0
- package/build/licenseStore.js +400 -30
- package/build/licenseStore.js.map +1 -0
- package/build/licenseTrustless.d.ts +43 -0
- package/build/licenseTrustless.d.ts.map +1 -0
- package/build/licenseTrustless.js +180 -0
- package/build/licenseTrustless.js.map +1 -0
- package/build/manifestVerify.d.ts +29 -1
- package/build/manifestVerify.d.ts.map +1 -0
- package/build/manifestVerify.js +46 -4
- package/build/manifestVerify.js.map +1 -0
- package/build/minaPriceHistory.d.ts +9 -0
- package/build/minaPriceHistory.d.ts.map +1 -0
- package/build/minaPriceHistory.js +152 -0
- package/build/minaPriceHistory.js.map +1 -0
- package/build/onChainRoot.d.ts +60 -0
- package/build/onChainRoot.d.ts.map +1 -0
- package/build/onChainRoot.js +74 -0
- package/build/onChainRoot.js.map +1 -0
- package/build/ownershipLicenseHash.d.ts +25 -0
- package/build/ownershipLicenseHash.d.ts.map +1 -0
- package/build/ownershipLicenseHash.js +29 -0
- package/build/ownershipLicenseHash.js.map +1 -0
- package/build/ownershipSignature.d.ts +153 -0
- package/build/ownershipSignature.d.ts.map +1 -0
- package/build/ownershipSignature.js +297 -0
- package/build/ownershipSignature.js.map +1 -0
- package/build/permitCheckFlow.d.ts +155 -0
- package/build/permitCheckFlow.d.ts.map +1 -0
- package/build/permitCheckFlow.js +196 -0
- package/build/permitCheckFlow.js.map +1 -0
- package/build/permitDescribe.d.ts +124 -0
- package/build/permitDescribe.d.ts.map +1 -0
- package/build/permitDescribe.js +324 -0
- package/build/permitDescribe.js.map +1 -0
- package/build/permitInterface.d.ts +85 -0
- package/build/permitInterface.d.ts.map +1 -0
- package/build/permitInterface.js +121 -0
- package/build/permitInterface.js.map +1 -0
- package/build/permitMessages.d.ts +313 -0
- package/build/permitMessages.d.ts.map +1 -0
- package/build/permitMessages.js +462 -0
- package/build/permitMessages.js.map +1 -0
- package/build/permitProof.d.ts +108 -0
- package/build/permitProof.d.ts.map +1 -0
- package/build/permitProof.js +202 -0
- package/build/permitProof.js.map +1 -0
- package/build/permitRegistryRebuild.d.ts +166 -0
- package/build/permitRegistryRebuild.d.ts.map +1 -0
- package/build/permitRegistryRebuild.js +347 -0
- package/build/permitRegistryRebuild.js.map +1 -0
- package/build/permitTree.d.ts +88 -0
- package/build/permitTree.d.ts.map +1 -0
- package/build/permitTree.js +230 -0
- package/build/permitTree.js.map +1 -0
- package/build/permitsClient.d.ts +680 -0
- package/build/permitsClient.d.ts.map +1 -0
- package/build/permitsClient.js +597 -0
- package/build/permitsClient.js.map +1 -0
- package/build/proveErrors.d.ts +50 -0
- package/build/proveErrors.d.ts.map +1 -0
- package/build/proveErrors.js +76 -0
- package/build/proveErrors.js.map +1 -0
- package/build/receipt.d.ts +95 -0
- package/build/receipt.d.ts.map +1 -0
- package/build/receipt.js +84 -0
- package/build/receipt.js.map +1 -0
- package/build/refundClient.d.ts +7 -12
- package/build/refundClient.d.ts.map +1 -0
- package/build/refundClient.js +52 -21
- package/build/refundClient.js.map +1 -0
- package/build/registryTrustVerify.d.ts +78 -0
- package/build/registryTrustVerify.d.ts.map +1 -0
- package/build/registryTrustVerify.js +129 -0
- package/build/registryTrustVerify.js.map +1 -0
- package/build/releaseClient.d.ts +26 -0
- package/build/releaseClient.d.ts.map +1 -0
- package/build/releaseClient.js +65 -0
- package/build/releaseClient.js.map +1 -0
- package/build/renewClient.d.ts +28 -2
- package/build/renewClient.d.ts.map +1 -0
- package/build/renewClient.js +31 -27
- package/build/renewClient.js.map +1 -0
- package/build/seatName.d.ts +8 -0
- package/build/seatName.d.ts.map +1 -0
- package/build/seatName.js +136 -0
- package/build/seatName.js.map +1 -0
- package/build/slotConstants.d.ts +15 -0
- package/build/slotConstants.d.ts.map +1 -0
- package/build/slotConstants.js +17 -0
- package/build/slotConstants.js.map +1 -0
- package/build/statsClient.d.ts +61 -7
- package/build/statsClient.d.ts.map +1 -0
- package/build/statsClient.js +44 -5
- package/build/statsClient.js.map +1 -0
- package/build/successorChain.d.ts +35 -0
- package/build/successorChain.d.ts.map +1 -0
- package/build/successorChain.js +69 -0
- package/build/successorChain.js.map +1 -0
- package/build/taxId.d.ts +44 -0
- package/build/taxId.d.ts.map +1 -0
- package/build/taxId.js +212 -0
- package/build/taxId.js.map +1 -0
- package/build/trustAnchorVerify.d.ts +49 -0
- package/build/trustAnchorVerify.d.ts.map +1 -0
- package/build/trustAnchorVerify.js +432 -0
- package/build/trustAnchorVerify.js.map +1 -0
- package/build/trustAnchorVerifyCli.d.ts +45 -0
- package/build/trustAnchorVerifyCli.d.ts.map +1 -0
- package/build/trustAnchorVerifyCli.js +235 -0
- package/build/trustAnchorVerifyCli.js.map +1 -0
- package/build/txBroadcastClient.d.ts +26 -0
- package/build/txBroadcastClient.d.ts.map +1 -0
- package/build/txBroadcastClient.js +20 -0
- package/build/txBroadcastClient.js.map +1 -0
- package/build/unbindClient.d.ts +77 -0
- package/build/unbindClient.d.ts.map +1 -0
- package/build/unbindClient.js +169 -0
- package/build/unbindClient.js.map +1 -0
- package/build/units.d.ts +28 -0
- package/build/units.d.ts.map +1 -0
- package/build/units.js +65 -0
- package/build/units.js.map +1 -0
- package/build/vatRates.d.ts +54 -0
- package/build/vatRates.d.ts.map +1 -0
- package/build/vatRates.js +82 -0
- package/build/vatRates.js.map +1 -0
- package/build/vatRatesData.d.ts +5 -0
- package/build/vatRatesData.d.ts.map +1 -0
- package/build/vatRatesData.js +42 -0
- package/build/vatRatesData.js.map +1 -0
- package/build/vendorAccountClient.d.ts +66 -0
- package/build/vendorAccountClient.d.ts.map +1 -0
- package/build/vendorAccountClient.js +47 -0
- package/build/vendorAccountClient.js.map +1 -0
- package/build/verifyCore.d.ts +4 -1
- package/build/verifyCore.d.ts.map +1 -0
- package/build/verifyCore.js +21 -10
- package/build/verifyCore.js.map +1 -0
- package/build/verifyOffline.d.ts +85 -0
- package/build/verifyOffline.d.ts.map +1 -0
- package/build/verifyOffline.js +139 -0
- package/build/verifyOffline.js.map +1 -0
- package/build/verifyService.d.ts.map +1 -0
- package/build/verifyService.js +571 -501
- package/build/verifyService.js.map +1 -0
- package/build/verifyWithAncestors.d.ts +19 -0
- package/build/verifyWithAncestors.d.ts.map +1 -0
- package/build/verifyWithAncestors.js +127 -0
- package/build/verifyWithAncestors.js.map +1 -0
- package/build/zipToState.d.ts +2 -0
- package/build/zipToState.d.ts.map +1 -0
- package/build/zipToState.js +94 -0
- package/build/zipToState.js.map +1 -0
- package/package.json +141 -16
- package/src/agent.ts +682 -0
- package/src/agentActions.ts +637 -0
- package/src/agentRecord.ts +121 -0
- package/src/agentVendor.ts +356 -0
- package/src/appsLookup.ts +48 -0
- package/src/archiveBootstrap.ts +386 -0
- package/src/atomicJson.ts +53 -0
- package/src/bindClient.ts +297 -0
- package/src/buyClient.ts +344 -0
- package/src/chainHead.ts +106 -0
- package/src/compactMerkleMap.ts +222 -0
- package/src/contractInterface.ts +465 -0
- package/src/contractTypes.ts +31 -0
- package/src/countryStats.ts +200 -0
- package/src/deployClient.ts +517 -0
- package/src/deviceIdentity.ts +249 -0
- package/src/freezeClient.ts +162 -0
- package/src/generationClient.ts +145 -0
- package/src/holderSecret.ts +91 -0
- package/src/index.ts +362 -0
- package/src/keysClient.ts +135 -0
- package/src/licenseLeaf.ts +221 -0
- package/src/licenseRecord.ts +190 -0
- package/src/licenseStore.ts +767 -0
- package/src/licenseTrustless.ts +207 -0
- package/src/manifestVerify.ts +164 -0
- package/src/minaPriceHistory.ts +166 -0
- package/src/onChainRoot.ts +90 -0
- package/src/ownershipLicenseHash.ts +31 -0
- package/src/ownershipSignature.ts +421 -0
- package/src/permitCheckFlow.ts +227 -0
- package/src/permitDescribe.ts +362 -0
- package/src/permitInterface.ts +146 -0
- package/src/permitMessages.ts +572 -0
- package/src/permitProof.ts +230 -0
- package/src/permitRegistryRebuild.ts +423 -0
- package/src/permitTree.ts +232 -0
- package/src/permitsClient.ts +746 -0
- package/src/proveErrors.ts +98 -0
- package/src/receipt.ts +156 -0
- package/src/refundClient.ts +177 -0
- package/src/registryTrustVerify.ts +192 -0
- package/src/releaseClient.ts +101 -0
- package/src/renewClient.ts +234 -0
- package/src/seatName.ts +165 -0
- package/src/slotConstants.ts +18 -0
- package/src/statsClient.ts +249 -0
- package/src/successorChain.ts +87 -0
- package/src/taxId.ts +227 -0
- package/src/trustAnchorVerify.ts +550 -0
- package/src/trustAnchorVerifyCli.ts +234 -0
- package/src/txBroadcastClient.ts +48 -0
- package/src/unbindClient.ts +233 -0
- package/src/units.ts +74 -0
- package/src/vatRates.ts +100 -0
- package/src/vatRatesData.ts +43 -0
- package/src/vendorAccountClient.ts +120 -0
- package/src/verifyCore.ts +100 -0
- package/src/verifyOffline.ts +212 -0
- package/src/verifyService.ts +982 -0
- package/src/verifyWithAncestors.ts +163 -0
- package/src/zipToState.ts +94 -0
- package/build/licenseProof.d.ts +0 -26
- package/build/licenseProof.js +0 -41
- package/build/migrateClient.d.ts +0 -43
- package/build/migrateClient.js +0 -47
- package/build/ownership.d.ts +0 -34
- 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
|
+
}
|