zklicensing 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +104 -0
- package/README.md +603 -0
- package/build/archiveBootstrap.d.ts +60 -0
- package/build/archiveBootstrap.js +234 -0
- package/build/buyClient.d.ts +58 -0
- package/build/buyClient.js +162 -0
- package/build/contractInterface.d.ts +55 -0
- package/build/contractInterface.js +213 -0
- package/build/countryStats.d.ts +37 -0
- package/build/countryStats.js +89 -0
- package/build/deployClient.d.ts +168 -0
- package/build/deployClient.js +172 -0
- package/build/freezeClient.d.ts +44 -0
- package/build/freezeClient.js +59 -0
- package/build/generationClient.d.ts +54 -0
- package/build/generationClient.js +113 -0
- package/build/index.d.ts +72 -0
- package/build/index.js +258 -0
- package/build/licenseProof.d.ts +26 -0
- package/build/licenseProof.js +41 -0
- package/build/licenseRecord.d.ts +39 -0
- package/build/licenseRecord.js +6 -0
- package/build/licenseStore.d.ts +49 -0
- package/build/licenseStore.js +303 -0
- package/build/manifestVerify.d.ts +30 -0
- package/build/manifestVerify.js +95 -0
- package/build/migrateClient.d.ts +43 -0
- package/build/migrateClient.js +47 -0
- package/build/ownership.d.ts +34 -0
- package/build/ownership.js +147 -0
- package/build/refundClient.d.ts +52 -0
- package/build/refundClient.js +92 -0
- package/build/renewClient.d.ts +49 -0
- package/build/renewClient.js +135 -0
- package/build/statsClient.d.ts +96 -0
- package/build/statsClient.js +28 -0
- package/build/verifyCore.d.ts +26 -0
- package/build/verifyCore.js +57 -0
- package/build/verifyService.d.ts +3 -0
- package/build/verifyService.js +690 -0
- package/package.json +118 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
/**
|
|
4
|
+
* contractInterface.ts
|
|
5
|
+
*
|
|
6
|
+
* Public contract interface: event Structs, slot/grace constants, duration
|
|
7
|
+
* helpers, and a minimal `LicensingAppEventsContract` SmartContract subclass
|
|
8
|
+
* that has the events declaration but no @method bodies.
|
|
9
|
+
*
|
|
10
|
+
* The full `LicensingApp` (with provable method bodies) lives in the private
|
|
11
|
+
* zklicensing-app package; only the things needed to decode events and reason
|
|
12
|
+
* about durations are published here so a public verify service can replay
|
|
13
|
+
* the archive without pulling in the contract source.
|
|
14
|
+
*
|
|
15
|
+
* `CANONICAL_VK_HASH` and `CANONICAL_LICENSE_PROOF_VK_HASH` are the
|
|
16
|
+
* verification-key hashes of the two published circuits — `LicensingApp`
|
|
17
|
+
* (the on-chain zkApp) and `LicenseOwnershipProgram` (the ownership
|
|
18
|
+
* challenge/response `ZkProgram`), respectively. Both are hand-maintained
|
|
19
|
+
* constants — bump them whenever the corresponding circuit changes and
|
|
20
|
+
* rerun the keeper's boot assertion (see zklicensing-app/src/keeperService.ts)
|
|
21
|
+
* / the verify service's `ensureCompiled()` check to catch drift.
|
|
22
|
+
*/
|
|
23
|
+
import { declareState, fetchAccount, Field, PublicKey, SmartContract, Struct, UInt32, UInt64, MerkleMap } from 'o1js';
|
|
24
|
+
// ------------------------------------------------------------
|
|
25
|
+
// EVENT STRUCTS
|
|
26
|
+
// ------------------------------------------------------------
|
|
27
|
+
// Struct() returns a class whose type references o1js internals that fall
|
|
28
|
+
// outside this package's rootDir (npm hoists o1js to the workspace root).
|
|
29
|
+
// Annotating each export as `any` keeps tsc from inferring (and therefore
|
|
30
|
+
// emitting) those internal type references. Consumers get full typing through
|
|
31
|
+
// the hand-written contractInterface.d.ts shim.
|
|
32
|
+
//
|
|
33
|
+
// The clean alternative — `export class X extends Struct({...}) {}` as the
|
|
34
|
+
// mina-fungible-token repo does — only works when o1js sits inside rootDir,
|
|
35
|
+
// which a workspace member can't guarantee.
|
|
36
|
+
export const LicenseIssuedEvent = Struct({
|
|
37
|
+
licenseHash: Field,
|
|
38
|
+
buyer: PublicKey,
|
|
39
|
+
amount: UInt64,
|
|
40
|
+
slot: UInt32,
|
|
41
|
+
expirySlot: UInt32,
|
|
42
|
+
});
|
|
43
|
+
export const ExpiryUpdateEvent = Struct({
|
|
44
|
+
licenseHash: Field,
|
|
45
|
+
newExpiry: UInt32,
|
|
46
|
+
});
|
|
47
|
+
export const PriceUpdatedEvent = Struct({
|
|
48
|
+
priceMonthly: UInt64,
|
|
49
|
+
priceYearly: UInt64,
|
|
50
|
+
priceFiveYear: UInt64,
|
|
51
|
+
});
|
|
52
|
+
export const AppCreatedEvent = Struct({
|
|
53
|
+
vendor: PublicKey,
|
|
54
|
+
});
|
|
55
|
+
// Emitted by `migrateLicense` when a license issued under an ancestor
|
|
56
|
+
// deployment is re-minted into this deployment at zero payment. `licenseHash`
|
|
57
|
+
// is stable across all deployments because the buyer's derived hash is what
|
|
58
|
+
// identifies the license; `expirySlot` is copied verbatim from the ancestor's
|
|
59
|
+
// licensesRoot entry; `ancestorGen` is the generation index the migration
|
|
60
|
+
// resolved against (needed so archive replay and auditors can trace which
|
|
61
|
+
// frozen root supplied the entry — skip-generation migrations are one-hop).
|
|
62
|
+
export const LicenseMigratedEvent = Struct({
|
|
63
|
+
licenseHash: Field,
|
|
64
|
+
expirySlot: UInt32,
|
|
65
|
+
ancestorGen: UInt32,
|
|
66
|
+
});
|
|
67
|
+
// ------------------------------------------------------------
|
|
68
|
+
// CONSTANTS (slot math — 90 s/slot after Mesa upgrade)
|
|
69
|
+
// ------------------------------------------------------------
|
|
70
|
+
export const MONTHLY_SLOTS_N = 28_800; // 30 days × 24 × 40
|
|
71
|
+
export const ONE_YEAR_SLOTS_N = 350_640; // 365.25 days × 24 × 40 (Julian year)
|
|
72
|
+
export const FIVE_YEAR_SLOTS_N = 1_753_200; // 5 × ONE_YEAR_SLOTS_N
|
|
73
|
+
export const REFUND_WINDOW_N = 13_440;
|
|
74
|
+
export const GRACE_PERIOD_N = 6_720;
|
|
75
|
+
export const MS_PER_SLOT_N = 90_000; // Mesa slot duration in ms
|
|
76
|
+
export const SLOTS_PER_DAY_N = 960; // 86_400_000 / MS_PER_SLOT_N
|
|
77
|
+
// Slot tolerance for the globalSlotSinceGenesis precondition. Applied uniformly to
|
|
78
|
+
// every method that reads currentSlot. 40 slots × 90 s = 1 hour — sized to
|
|
79
|
+
// clear the honest-user prove+wallet+mempool latency envelope. The refund vs
|
|
80
|
+
// release windows stay disjoint by construction: requestRefund asserts against
|
|
81
|
+
// the lower bound (currentSlot ≤ purchaseSlot + REFUND_WINDOW), releaseFunds
|
|
82
|
+
// asserts against a threshold shifted by +TOLERANCE_WINDOW so neither can
|
|
83
|
+
// land on the same real slot as the other. See AUDIT_BRIEF.md §7.
|
|
84
|
+
export const TOLERANCE_WINDOW_N = 40;
|
|
85
|
+
export const EMPTY = Field(0);
|
|
86
|
+
export const EMPTY_ROOT = new MerkleMap().getRoot();
|
|
87
|
+
// ------------------------------------------------------------
|
|
88
|
+
// DURATION HELPERS
|
|
89
|
+
// ------------------------------------------------------------
|
|
90
|
+
export const DURATION_LABELS = ['1 Month', '1 Year', '5 Years'];
|
|
91
|
+
export const DURATION_INDEX = { '1 Month': 0, '1 Year': 1, '5 Years': 2 };
|
|
92
|
+
export function durationSlotsFromLabel(label) {
|
|
93
|
+
if (label === '1 Month')
|
|
94
|
+
return MONTHLY_SLOTS_N;
|
|
95
|
+
if (label === '1 Year')
|
|
96
|
+
return ONE_YEAR_SLOTS_N;
|
|
97
|
+
if (label === '5 Years')
|
|
98
|
+
return FIVE_YEAR_SLOTS_N;
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
export function durationIndexFromLabel(label) {
|
|
102
|
+
if (label === '1 Month')
|
|
103
|
+
return 0;
|
|
104
|
+
if (label === '1 Year')
|
|
105
|
+
return 1;
|
|
106
|
+
if (label === '5 Years')
|
|
107
|
+
return 2;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
export function durationLabelFromIndex(idx) {
|
|
111
|
+
return DURATION_LABELS[idx] ?? null;
|
|
112
|
+
}
|
|
113
|
+
// ------------------------------------------------------------
|
|
114
|
+
// EVENTS-ONLY CONTRACT
|
|
115
|
+
//
|
|
116
|
+
// A SmartContract subclass with the same `events: { ... }` declaration as
|
|
117
|
+
// LicensingApp but no `@method` bodies. Used by archive-replay code to call
|
|
118
|
+
// `.fetchEvents()` without depending on the private circuit source.
|
|
119
|
+
// ------------------------------------------------------------
|
|
120
|
+
export class LicensingAppEventsContract extends SmartContract {
|
|
121
|
+
events = {
|
|
122
|
+
appCreated: AppCreatedEvent,
|
|
123
|
+
licenseIssued: LicenseIssuedEvent,
|
|
124
|
+
licenseIssuedNoEscrow: LicenseIssuedEvent,
|
|
125
|
+
licenseRenewed: ExpiryUpdateEvent,
|
|
126
|
+
licenseMigrated: LicenseMigratedEvent,
|
|
127
|
+
refundIssued: Field,
|
|
128
|
+
fundsReleased: Field,
|
|
129
|
+
priceUpdated: PriceUpdatedEvent,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
// State-slot layout mirrors LicensingApp's declareState(). Read-only from here
|
|
133
|
+
// — no @method bodies exist. Consumers use `.version.get()`, `.licensesRoot.get()`
|
|
134
|
+
// etc. after fetchAccount(). `pricesPacked` is a single Field with the three
|
|
135
|
+
// UInt64 tier prices packed as [pM | pY<<64 | pF<<128]; unpack with
|
|
136
|
+
// `unpackPrices` if you need the individual tiers off-chain.
|
|
137
|
+
declareState(LicensingAppEventsContract, {
|
|
138
|
+
licensesRoot: Field,
|
|
139
|
+
vendor: PublicKey,
|
|
140
|
+
escrowRoot: Field,
|
|
141
|
+
pricesPacked: Field,
|
|
142
|
+
version: UInt32,
|
|
143
|
+
ancestorsRoot: Field,
|
|
144
|
+
});
|
|
145
|
+
// Off-chain mirror of the in-circuit price packing (see LicensingApp.ts). Kept
|
|
146
|
+
// here so the events service can decode `pricesPacked` slots without pulling
|
|
147
|
+
// the private contract source.
|
|
148
|
+
export function unpackPrices(packed) {
|
|
149
|
+
const raw = packed.toBigInt();
|
|
150
|
+
const mask = (1n << 64n) - 1n;
|
|
151
|
+
return {
|
|
152
|
+
priceMonthly: UInt64.from(raw & mask),
|
|
153
|
+
priceYearly: UInt64.from((raw >> 64n) & mask),
|
|
154
|
+
priceFiveYear: UInt64.from((raw >> 128n) & mask),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// ------------------------------------------------------------
|
|
158
|
+
// SUPPORTED SOURCE VERSIONS
|
|
159
|
+
//
|
|
160
|
+
// On-chain `version` slot is set at init() and can never change (setPermissions
|
|
161
|
+
// is impossible; setVerificationKey is impossibleDuringCurrentVersion). A
|
|
162
|
+
// keeper / verifier that only understands v1 state decodes must refuse to serve
|
|
163
|
+
// a zkApp deployed from a different source revision — otherwise it could
|
|
164
|
+
// mis-decode events or feed a stale state format to the verifier.
|
|
165
|
+
//
|
|
166
|
+
// Add new versions here once the corresponding decoder / verifier code path
|
|
167
|
+
// lands. Never remove: dropping a version is a support-removal decision that
|
|
168
|
+
// belongs in a release note, not silent breakage.
|
|
169
|
+
// ------------------------------------------------------------
|
|
170
|
+
export const SUPPORTED_LICENSING_APP_VERSIONS = [1];
|
|
171
|
+
// Read the immutable `version` slot from a deployed LicensingApp. Returns null
|
|
172
|
+
// when the account is not (yet) a zkApp — caller decides whether to retry
|
|
173
|
+
// (deploy still pending) or reject (wrong address).
|
|
174
|
+
export async function fetchLicensingAppVersion(zkAppAddress) {
|
|
175
|
+
const pub = PublicKey.fromBase58(zkAppAddress);
|
|
176
|
+
await fetchAccount({ publicKey: pub });
|
|
177
|
+
const contract = new LicensingAppEventsContract(pub);
|
|
178
|
+
try {
|
|
179
|
+
const v = contract.version.get();
|
|
180
|
+
return Number(v.toString());
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ------------------------------------------------------------
|
|
187
|
+
// CANONICAL VK HASHES
|
|
188
|
+
//
|
|
189
|
+
// Per-generation, append-only. The keeper boots with `LicensingApp.compile()`
|
|
190
|
+
// and asserts the resulting VK hash matches `CANONICAL_VK_HASH` (the current
|
|
191
|
+
// generation's row); the verify service resolves an address's generation via
|
|
192
|
+
// AppRecord.deploymentHistory and asserts the on-chain VK hash matches
|
|
193
|
+
// `CANONICAL_VK_HASHES[generation]`. Retired-generation licenses (e.g. a
|
|
194
|
+
// 5-year buyer whose license lives on v1 after the platform bumped to v2/v3)
|
|
195
|
+
// continue to verify because the historical row is still present. A genuine
|
|
196
|
+
// old circuit deployed at a fresh address fails: the address is not in any
|
|
197
|
+
// `deploymentHistory[]`, so the resolver returns null and verify rejects.
|
|
198
|
+
//
|
|
199
|
+
// Rules:
|
|
200
|
+
// - Append the new generation's row at each hardfork migration; never
|
|
201
|
+
// delete historical rows.
|
|
202
|
+
// - Bump `CURRENT_GENERATION` to the new key alongside the row.
|
|
203
|
+
// - `CANONICAL_VK_HASH` is derived from the record — drift test and keeper
|
|
204
|
+
// boot-assert are unchanged.
|
|
205
|
+
// ------------------------------------------------------------
|
|
206
|
+
export const CANONICAL_VK_HASHES = {
|
|
207
|
+
1: '25510637873522061606403949338800808972643356911784243535869508122414992770753',
|
|
208
|
+
// 2: '…' — append at each hardfork migration, never delete.
|
|
209
|
+
};
|
|
210
|
+
export const CURRENT_GENERATION = 1;
|
|
211
|
+
export const CANONICAL_VK_HASH = CANONICAL_VK_HASHES[CURRENT_GENERATION];
|
|
212
|
+
export const CANONICAL_LICENSE_PROOF_VK_HASH = '6592142093294558010952934797827640785154417146015648013584661926355710165692';
|
|
213
|
+
//# sourceMappingURL=contractInterface.js.map
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* countryStats.ts
|
|
3
|
+
*
|
|
4
|
+
* Buyer-country breakdown of a vendor's revenue, bucketed by UTC calendar year.
|
|
5
|
+
* Structured for tax reporting: one row per (year, appId, countryCode) with
|
|
6
|
+
* license count + the vendor's cut in MINA. Both the keeper (source of
|
|
7
|
+
* truth for /stats) and the dashboard UI import from here so the numbers can
|
|
8
|
+
* never drift between them.
|
|
9
|
+
*
|
|
10
|
+
* Purchases attribute to the year the buyer confirmed on-chain (LicenseRecord
|
|
11
|
+
* `createdAt` ISO timestamp). Legacy records that predate `createdAt` fall
|
|
12
|
+
* back to slot-derived time. Renewals attribute to the renewal year via
|
|
13
|
+
* `renewals[i].slot → time`. Refunded purchases contribute zero revenue but
|
|
14
|
+
* still count the license (a purchase happened even if the money was
|
|
15
|
+
* returned) — remove `count` from refunded rows client-side if you want a
|
|
16
|
+
* strictly net view.
|
|
17
|
+
*
|
|
18
|
+
* Browser-safe: pure computation, no fs, no fetch.
|
|
19
|
+
*/
|
|
20
|
+
import type { LicenseRecord } from './licenseRecord.js';
|
|
21
|
+
export type CountryBucket = {
|
|
22
|
+
count: number;
|
|
23
|
+
revenueMina: number;
|
|
24
|
+
};
|
|
25
|
+
export type CountryYearBreakdown = {
|
|
26
|
+
aggregate: Record<string, CountryBucket>;
|
|
27
|
+
byApp: Record<string, Record<string, CountryBucket>>;
|
|
28
|
+
};
|
|
29
|
+
export type CountryBreakdown = {
|
|
30
|
+
years: number[];
|
|
31
|
+
byYear: Record<number, CountryYearBreakdown>;
|
|
32
|
+
};
|
|
33
|
+
export declare function computeCountryBreakdown(apps: {
|
|
34
|
+
appId: string;
|
|
35
|
+
zkAppAddress: string;
|
|
36
|
+
}[], records: LicenseRecord[], currentSlot: number, nowMs: number): CountryBreakdown;
|
|
37
|
+
//# sourceMappingURL=countryStats.d.ts.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
// LicensingApp charges a 2 % platform fee (200 basis points); the vendor
|
|
4
|
+
// receives the remaining 9 800 bps. Keep this in sync with the on-chain
|
|
5
|
+
// constant in LicensingApp.
|
|
6
|
+
const PLATFORM_FEE_BASIS_POINTS = 200n;
|
|
7
|
+
const VENDOR_BPS = 10000n - PLATFORM_FEE_BASIS_POINTS;
|
|
8
|
+
// 90 s/slot post-Mesa. Used only for slot→time fallback attribution.
|
|
9
|
+
const SLOT_DURATION_MS = 90_000;
|
|
10
|
+
export function computeCountryBreakdown(apps, records, currentSlot, nowMs) {
|
|
11
|
+
const appByAddress = new Map(apps.map(a => [a.zkAppAddress, a.appId]));
|
|
12
|
+
const byYear = {};
|
|
13
|
+
const yearSet = new Set();
|
|
14
|
+
const ensureYear = (year) => {
|
|
15
|
+
let yb = byYear[year];
|
|
16
|
+
if (!yb) {
|
|
17
|
+
yb = { aggregate: {}, byApp: {} };
|
|
18
|
+
byYear[year] = yb;
|
|
19
|
+
yearSet.add(year);
|
|
20
|
+
}
|
|
21
|
+
return yb;
|
|
22
|
+
};
|
|
23
|
+
const ensureAppBucket = (yb, appId) => {
|
|
24
|
+
let ab = yb.byApp[appId];
|
|
25
|
+
if (!ab) {
|
|
26
|
+
ab = {};
|
|
27
|
+
yb.byApp[appId] = ab;
|
|
28
|
+
}
|
|
29
|
+
return ab;
|
|
30
|
+
};
|
|
31
|
+
const add = (bucket, code, count, revenueNano) => {
|
|
32
|
+
let b = bucket[code];
|
|
33
|
+
if (!b) {
|
|
34
|
+
b = { count: 0, revenueMina: 0 };
|
|
35
|
+
bucket[code] = b;
|
|
36
|
+
}
|
|
37
|
+
b.count += count;
|
|
38
|
+
b.revenueMina += Number(revenueNano) / 1e9;
|
|
39
|
+
};
|
|
40
|
+
const slotToYear = (slot) => {
|
|
41
|
+
if (currentSlot <= 0)
|
|
42
|
+
return null;
|
|
43
|
+
const ms = nowMs - (currentSlot - slot) * SLOT_DURATION_MS;
|
|
44
|
+
const y = new Date(ms).getUTCFullYear();
|
|
45
|
+
return Number.isFinite(y) ? y : null;
|
|
46
|
+
};
|
|
47
|
+
for (const r of records) {
|
|
48
|
+
const appId = appByAddress.get(r.zkAppAddress);
|
|
49
|
+
if (!appId)
|
|
50
|
+
continue;
|
|
51
|
+
const country = r.buyerCountry && /^[A-Z]{2}$/.test(r.buyerCountry) ? r.buyerCountry : '??';
|
|
52
|
+
// Purchase — attribute to the year of confirm-on-chain (createdAt).
|
|
53
|
+
// Fallback: slot-derived time for legacy records that lack createdAt.
|
|
54
|
+
let purchaseYear = null;
|
|
55
|
+
if (r.createdAt) {
|
|
56
|
+
const y = new Date(r.createdAt).getUTCFullYear();
|
|
57
|
+
if (Number.isFinite(y))
|
|
58
|
+
purchaseYear = y;
|
|
59
|
+
}
|
|
60
|
+
if (purchaseYear === null && r.purchaseSlot > 0) {
|
|
61
|
+
purchaseYear = slotToYear(r.purchaseSlot);
|
|
62
|
+
}
|
|
63
|
+
if (purchaseYear !== null) {
|
|
64
|
+
const yb = ensureYear(purchaseYear);
|
|
65
|
+
const ab = ensureAppBucket(yb, appId);
|
|
66
|
+
const purchaseCut = (!r.refunded && r.purchaseAmount && r.purchaseAmount !== '0')
|
|
67
|
+
? (BigInt(r.purchaseAmount) * VENDOR_BPS) / 10000n
|
|
68
|
+
: 0n;
|
|
69
|
+
add(ab, country, 1, purchaseCut);
|
|
70
|
+
add(yb.aggregate, country, 1, purchaseCut);
|
|
71
|
+
}
|
|
72
|
+
// Renewals — revenue only (no license count), attributed to renewal year.
|
|
73
|
+
for (const rn of r.renewals ?? []) {
|
|
74
|
+
const y = slotToYear(rn.slot);
|
|
75
|
+
if (y === null)
|
|
76
|
+
continue;
|
|
77
|
+
const yb = ensureYear(y);
|
|
78
|
+
const ab = ensureAppBucket(yb, appId);
|
|
79
|
+
const cut = (BigInt(rn.amount) * VENDOR_BPS) / 10000n;
|
|
80
|
+
add(ab, country, 0, cut);
|
|
81
|
+
add(yb.aggregate, country, 0, cut);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
years: Array.from(yearSet).sort((a, b) => b - a),
|
|
86
|
+
byYear,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=countryStats.js.map
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deployClient.ts
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe client for the deploy flow. Mirrors the buy / renew / refund
|
|
5
|
+
* client shape: `proveDeploy` posts to the keeper's `/prove/deploy` endpoint
|
|
6
|
+
* and returns the atomic deploy+initialize tx already signed with the
|
|
7
|
+
* throwaway zkApp key. The vendor's wallet then adds the fee-payer signature
|
|
8
|
+
* and broadcasts.
|
|
9
|
+
*
|
|
10
|
+
* The atomic bundling is what makes single-shot initialization safe:
|
|
11
|
+
* `provenTxJson` holds both `zkApp.deploy()` and `zkApp.initialize()`
|
|
12
|
+
* AccountUpdates in the same transaction, and the keeper generates a
|
|
13
|
+
* throwaway zkApp keypair per request that is discarded before the tx is
|
|
14
|
+
* returned — so the target address is not observable to an attacker until
|
|
15
|
+
* the initialize AU inside is already signed and locked to `(vendor,
|
|
16
|
+
* prices)`. Combined with the in-circuit `vendor.requireEquals(empty())`
|
|
17
|
+
* precondition on `initialize()`, this yields a one-shot init per account.
|
|
18
|
+
*
|
|
19
|
+
* Also exports admin-side `redeployApp` / `redeployAll` for the platform
|
|
20
|
+
* operator to trigger post-hardfork v1→v2 migrations via the keeper's
|
|
21
|
+
* `/admin/redeploy*` endpoints (bearer-token auth).
|
|
22
|
+
*/
|
|
23
|
+
export type ProveDeployRequest = {
|
|
24
|
+
keeperUrl: string;
|
|
25
|
+
senderPublicKey: string;
|
|
26
|
+
vendorAddress: string;
|
|
27
|
+
priceMonthlyMina: number;
|
|
28
|
+
priceYearlyMina: number;
|
|
29
|
+
priceFiveYearMina: number;
|
|
30
|
+
fetcher?: typeof fetch;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
};
|
|
33
|
+
export type ProveDeployResponse = {
|
|
34
|
+
provenTxJson: string;
|
|
35
|
+
verificationKeyHash: string;
|
|
36
|
+
zkAppAddress: string;
|
|
37
|
+
};
|
|
38
|
+
export type DeployStatus = {
|
|
39
|
+
status: 'unknown' | 'pending' | 'confirmed' | 'failed';
|
|
40
|
+
landedAtSlot?: number;
|
|
41
|
+
elapsedMs?: number;
|
|
42
|
+
};
|
|
43
|
+
export declare function proveDeploy(req: ProveDeployRequest): Promise<ProveDeployResponse>;
|
|
44
|
+
export type PollDeployStatusOptions = {
|
|
45
|
+
keeperUrl: string;
|
|
46
|
+
zkAppAddress: string;
|
|
47
|
+
fetcher?: typeof fetch;
|
|
48
|
+
intervalMs?: number;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
onTick?: (status: DeployStatus) => void;
|
|
52
|
+
};
|
|
53
|
+
export declare function pollDeployStatus(opts: PollDeployStatusOptions): Promise<DeployStatus>;
|
|
54
|
+
export type RedeployAppRequest = {
|
|
55
|
+
keeperUrl: string;
|
|
56
|
+
appId: string;
|
|
57
|
+
adminToken: string;
|
|
58
|
+
force?: boolean;
|
|
59
|
+
fetcher?: typeof fetch;
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
};
|
|
62
|
+
export type RedeployAppResponse = {
|
|
63
|
+
ok: true;
|
|
64
|
+
zkAppAddress: string;
|
|
65
|
+
txHash: string;
|
|
66
|
+
verificationKeyHash: string;
|
|
67
|
+
onChainVersion: number;
|
|
68
|
+
forcedEscrowNonEmpty?: true;
|
|
69
|
+
};
|
|
70
|
+
export type RedeployAllRequest = {
|
|
71
|
+
keeperUrl: string;
|
|
72
|
+
adminToken: string;
|
|
73
|
+
force?: boolean;
|
|
74
|
+
fetcher?: typeof fetch;
|
|
75
|
+
signal?: AbortSignal;
|
|
76
|
+
};
|
|
77
|
+
export type RedeployAllResponse = {
|
|
78
|
+
ok: true;
|
|
79
|
+
count: number;
|
|
80
|
+
succeeded: number;
|
|
81
|
+
results: Array<{
|
|
82
|
+
appId: string;
|
|
83
|
+
ok: true;
|
|
84
|
+
zkAppAddress: string;
|
|
85
|
+
txHash: string;
|
|
86
|
+
verificationKeyHash: string;
|
|
87
|
+
onChainVersion: number;
|
|
88
|
+
forcedEscrowNonEmpty?: true;
|
|
89
|
+
} | {
|
|
90
|
+
appId: string;
|
|
91
|
+
ok: false;
|
|
92
|
+
error: string;
|
|
93
|
+
}>;
|
|
94
|
+
};
|
|
95
|
+
export declare function redeployApp(req: RedeployAppRequest): Promise<RedeployAppResponse>;
|
|
96
|
+
export declare function redeployAll(req: RedeployAllRequest): Promise<RedeployAllResponse>;
|
|
97
|
+
export type VendorRedeployPreflight = {
|
|
98
|
+
ok: boolean;
|
|
99
|
+
freezeSlotConfigured: boolean;
|
|
100
|
+
appExists: boolean;
|
|
101
|
+
status?: 'active' | 'inactive' | 'pending';
|
|
102
|
+
refundWindow: {
|
|
103
|
+
closed: boolean;
|
|
104
|
+
opensAtSlot: number;
|
|
105
|
+
currentSlot: number;
|
|
106
|
+
};
|
|
107
|
+
onChainAccount: {
|
|
108
|
+
exists: boolean;
|
|
109
|
+
};
|
|
110
|
+
escrow: {
|
|
111
|
+
empty: boolean;
|
|
112
|
+
};
|
|
113
|
+
version: {
|
|
114
|
+
current: number;
|
|
115
|
+
supported: boolean;
|
|
116
|
+
supportedList: readonly number[];
|
|
117
|
+
};
|
|
118
|
+
reasons: string[];
|
|
119
|
+
};
|
|
120
|
+
export declare function fetchVendorRedeployPreflight(opts: {
|
|
121
|
+
keeperUrl: string;
|
|
122
|
+
appId: string;
|
|
123
|
+
fetcher?: typeof fetch;
|
|
124
|
+
signal?: AbortSignal;
|
|
125
|
+
}): Promise<VendorRedeployPreflight>;
|
|
126
|
+
export type VendorRedeployChallenge = {
|
|
127
|
+
nonce: string;
|
|
128
|
+
expiresAt: number;
|
|
129
|
+
};
|
|
130
|
+
export declare function getVendorRedeployChallenge(opts: {
|
|
131
|
+
keeperUrl: string;
|
|
132
|
+
appId: string;
|
|
133
|
+
fetcher?: typeof fetch;
|
|
134
|
+
signal?: AbortSignal;
|
|
135
|
+
}): Promise<VendorRedeployChallenge>;
|
|
136
|
+
export type ProveVendorRedeployRequest = {
|
|
137
|
+
keeperUrl: string;
|
|
138
|
+
appId: string;
|
|
139
|
+
nonce: string;
|
|
140
|
+
signature: {
|
|
141
|
+
field: string;
|
|
142
|
+
scalar: string;
|
|
143
|
+
};
|
|
144
|
+
senderPublicKey: string;
|
|
145
|
+
force?: boolean;
|
|
146
|
+
fetcher?: typeof fetch;
|
|
147
|
+
signal?: AbortSignal;
|
|
148
|
+
};
|
|
149
|
+
export type ProveVendorRedeployResponse = {
|
|
150
|
+
provenTxJson: string;
|
|
151
|
+
zkAppAddress: string;
|
|
152
|
+
verificationKeyHash: string;
|
|
153
|
+
redeployId: string;
|
|
154
|
+
forcedEscrowNonEmpty?: true;
|
|
155
|
+
};
|
|
156
|
+
export declare function proveVendorRedeploy(req: ProveVendorRedeployRequest): Promise<ProveVendorRedeployResponse>;
|
|
157
|
+
export type RegisterVendorRedeployRequest = {
|
|
158
|
+
keeperUrl: string;
|
|
159
|
+
appId: string;
|
|
160
|
+
redeployId: string;
|
|
161
|
+
txHash: string;
|
|
162
|
+
fetcher?: typeof fetch;
|
|
163
|
+
signal?: AbortSignal;
|
|
164
|
+
};
|
|
165
|
+
export declare function registerVendorRedeploy(req: RegisterVendorRedeployRequest): Promise<{
|
|
166
|
+
ok: true;
|
|
167
|
+
}>;
|
|
168
|
+
//# sourceMappingURL=deployClient.d.ts.map
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// All rights reserved.
|
|
3
|
+
function baseUrl(url) {
|
|
4
|
+
return url.replace(/\/+$/, '');
|
|
5
|
+
}
|
|
6
|
+
async function readJsonError(resp) {
|
|
7
|
+
try {
|
|
8
|
+
const body = await resp.json();
|
|
9
|
+
if (typeof body.error === 'string')
|
|
10
|
+
return body.error;
|
|
11
|
+
}
|
|
12
|
+
catch { /* body wasn't JSON */ }
|
|
13
|
+
return `HTTP ${resp.status}`;
|
|
14
|
+
}
|
|
15
|
+
// POST /prove/deploy — keeper generates the throwaway zkApp keypair, assembles
|
|
16
|
+
// the deploy+initialize tx, proves it, and signs the zkApp AccountUpdate with
|
|
17
|
+
// the throwaway key. The caller forwards `provenTxJson` to the vendor's wallet
|
|
18
|
+
// for the fee-payer signature.
|
|
19
|
+
//
|
|
20
|
+
// At least one of the three prices must be > 0 (enforced by the keeper *and*
|
|
21
|
+
// by the circuit's `initialize()`).
|
|
22
|
+
export async function proveDeploy(req) {
|
|
23
|
+
const fetcher = req.fetcher ?? fetch;
|
|
24
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/prove/deploy`, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
signal: req.signal,
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
senderPublicKey: req.senderPublicKey,
|
|
30
|
+
vendorAddress: req.vendorAddress,
|
|
31
|
+
priceMonthlyMina: req.priceMonthlyMina,
|
|
32
|
+
priceYearlyMina: req.priceYearlyMina,
|
|
33
|
+
priceFiveYearMina: req.priceFiveYearMina,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
if (!resp.ok)
|
|
37
|
+
throw new Error(await readJsonError(resp));
|
|
38
|
+
return await resp.json();
|
|
39
|
+
}
|
|
40
|
+
// Polls GET /deploy/status until it returns a terminal state ('confirmed' or
|
|
41
|
+
// 'failed') or the timeout fires. 'unknown' means the keeper hasn't seen the
|
|
42
|
+
// tx yet (register step not called or watch not armed).
|
|
43
|
+
export async function pollDeployStatus(opts) {
|
|
44
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
45
|
+
const interval = opts.intervalMs ?? 3_000;
|
|
46
|
+
const timeout = opts.timeoutMs ?? 10 * 60 * 1000;
|
|
47
|
+
const deadline = Date.now() + timeout;
|
|
48
|
+
const url = `${baseUrl(opts.keeperUrl)}/deploy/status?zkAppAddress=${encodeURIComponent(opts.zkAppAddress)}`;
|
|
49
|
+
while (true) {
|
|
50
|
+
if (opts.signal?.aborted)
|
|
51
|
+
throw new Error('pollDeployStatus aborted');
|
|
52
|
+
let status = { status: 'unknown' };
|
|
53
|
+
try {
|
|
54
|
+
const resp = await fetcher(url, { signal: opts.signal });
|
|
55
|
+
if (resp.ok)
|
|
56
|
+
status = await resp.json();
|
|
57
|
+
}
|
|
58
|
+
catch { /* transient network — try again on next tick */ }
|
|
59
|
+
opts.onTick?.(status);
|
|
60
|
+
if (status.status === 'confirmed' || status.status === 'failed')
|
|
61
|
+
return status;
|
|
62
|
+
if (Date.now() >= deadline)
|
|
63
|
+
return status;
|
|
64
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function adminHeaders(token) {
|
|
68
|
+
return {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Authorization': `Bearer ${token}`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// POST /admin/redeploy/:appId — one-shot migration for a single app. Rejects
|
|
74
|
+
// with a descriptive error if any keeper-side guard fails (409) or the deploy
|
|
75
|
+
// itself throws (500). The returned `zkAppAddress` is the *new* v2 address; the
|
|
76
|
+
// old one stays on chain but is no longer referenced by keeper records.
|
|
77
|
+
export async function redeployApp(req) {
|
|
78
|
+
const fetcher = req.fetcher ?? fetch;
|
|
79
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/admin/redeploy/${encodeURIComponent(req.appId)}`, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: adminHeaders(req.adminToken),
|
|
82
|
+
signal: req.signal,
|
|
83
|
+
body: JSON.stringify(req.force ? { force: true } : {}),
|
|
84
|
+
});
|
|
85
|
+
if (!resp.ok)
|
|
86
|
+
throw new Error(await readJsonError(resp));
|
|
87
|
+
return await resp.json();
|
|
88
|
+
}
|
|
89
|
+
// POST /admin/redeploy-all — iterate every registered app and attempt migration.
|
|
90
|
+
// Idempotent: apps already migrated surface as ok:false with a clear error, so
|
|
91
|
+
// a rerun after a partial failure retries only the remaining ones. Callers
|
|
92
|
+
// should inspect `results` for per-app outcomes rather than relying on the
|
|
93
|
+
// top-level `ok`, which is always true unless the whole request failed.
|
|
94
|
+
export async function redeployAll(req) {
|
|
95
|
+
const fetcher = req.fetcher ?? fetch;
|
|
96
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/admin/redeploy-all`, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: adminHeaders(req.adminToken),
|
|
99
|
+
signal: req.signal,
|
|
100
|
+
body: JSON.stringify(req.force ? { force: true } : {}),
|
|
101
|
+
});
|
|
102
|
+
if (!resp.ok)
|
|
103
|
+
throw new Error(await readJsonError(resp));
|
|
104
|
+
return await resp.json();
|
|
105
|
+
}
|
|
106
|
+
// GET /vendor/redeploy/:appId/preflight — read-only guard status. No nonce
|
|
107
|
+
// consumed, no signature required. UI decides button state from the returned
|
|
108
|
+
// fields; 404 means the app itself is unknown (a distinct failure mode from a
|
|
109
|
+
// known app failing individual guards).
|
|
110
|
+
export async function fetchVendorRedeployPreflight(opts) {
|
|
111
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
112
|
+
const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/vendor/redeploy/${encodeURIComponent(opts.appId)}/preflight`, {
|
|
113
|
+
signal: opts.signal,
|
|
114
|
+
});
|
|
115
|
+
if (!resp.ok)
|
|
116
|
+
throw new Error(await readJsonError(resp));
|
|
117
|
+
return await resp.json();
|
|
118
|
+
}
|
|
119
|
+
// GET /vendor/redeploy/:appId/challenge — mint a one-shot nonce that the
|
|
120
|
+
// vendor's wallet signs. The nonce is bound to the appId and expires in 60s;
|
|
121
|
+
// consumed on /prove.
|
|
122
|
+
export async function getVendorRedeployChallenge(opts) {
|
|
123
|
+
const fetcher = opts.fetcher ?? fetch;
|
|
124
|
+
const resp = await fetcher(`${baseUrl(opts.keeperUrl)}/vendor/redeploy/${encodeURIComponent(opts.appId)}/challenge`, {
|
|
125
|
+
signal: opts.signal,
|
|
126
|
+
});
|
|
127
|
+
if (!resp.ok)
|
|
128
|
+
throw new Error(await readJsonError(resp));
|
|
129
|
+
return await resp.json();
|
|
130
|
+
}
|
|
131
|
+
// POST /vendor/redeploy/:appId/prove — keeper verifies the nonce + signature
|
|
132
|
+
// against the on-chain vendor of the app, runs all redeploy guards, generates
|
|
133
|
+
// a throwaway zkApp keypair, builds and proves the deploy+initialize tx, and
|
|
134
|
+
// signs the zkApp AU with the throwaway key. Returns the tx JSON for the
|
|
135
|
+
// vendor's wallet to add the fee-payer signature and broadcast.
|
|
136
|
+
export async function proveVendorRedeploy(req) {
|
|
137
|
+
const fetcher = req.fetcher ?? fetch;
|
|
138
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/vendor/redeploy/${encodeURIComponent(req.appId)}/prove`, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: { 'Content-Type': 'application/json' },
|
|
141
|
+
signal: req.signal,
|
|
142
|
+
body: JSON.stringify({
|
|
143
|
+
nonce: req.nonce,
|
|
144
|
+
signature: req.signature,
|
|
145
|
+
senderPublicKey: req.senderPublicKey,
|
|
146
|
+
...(req.force ? { force: true } : {}),
|
|
147
|
+
}),
|
|
148
|
+
});
|
|
149
|
+
if (!resp.ok)
|
|
150
|
+
throw new Error(await readJsonError(resp));
|
|
151
|
+
return await resp.json();
|
|
152
|
+
}
|
|
153
|
+
// POST /vendor/redeploy/:appId/register — arm the keeper's deploy-watcher on
|
|
154
|
+
// the new address. On confirmation the keeper mutates AppRecord to promote
|
|
155
|
+
// the new deployment (push old to deploymentHistory[], overwrite live fields,
|
|
156
|
+
// clear event-sync cursors). Idempotent to duplicate calls (the watcher
|
|
157
|
+
// checks whether one is already armed for the address). Poll GET
|
|
158
|
+
// /deploy/status?zkAppAddress=<new> — or use pollDeployStatus — for landing
|
|
159
|
+
// progress, same as first-time deploys.
|
|
160
|
+
export async function registerVendorRedeploy(req) {
|
|
161
|
+
const fetcher = req.fetcher ?? fetch;
|
|
162
|
+
const resp = await fetcher(`${baseUrl(req.keeperUrl)}/vendor/redeploy/${encodeURIComponent(req.appId)}/register`, {
|
|
163
|
+
method: 'POST',
|
|
164
|
+
headers: { 'Content-Type': 'application/json' },
|
|
165
|
+
signal: req.signal,
|
|
166
|
+
body: JSON.stringify({ redeployId: req.redeployId, txHash: req.txHash }),
|
|
167
|
+
});
|
|
168
|
+
if (!resp.ok)
|
|
169
|
+
throw new Error(await readJsonError(resp));
|
|
170
|
+
return await resp.json();
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=deployClient.js.map
|