@lionden/network 0.1.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 +201 -0
- package/README.md +23 -0
- package/dist/accounts.d.ts +12 -0
- package/dist/accounts.d.ts.map +1 -0
- package/dist/accounts.js +38 -0
- package/dist/accounts.js.map +1 -0
- package/dist/connection.d.ts +121 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +1064 -0
- package/dist/connection.js.map +1 -0
- package/dist/devnode-backend.d.ts +53 -0
- package/dist/devnode-backend.d.ts.map +1 -0
- package/dist/devnode-backend.js +121 -0
- package/dist/devnode-backend.js.map +1 -0
- package/dist/devnode-manager.d.ts +130 -0
- package/dist/devnode-manager.d.ts.map +1 -0
- package/dist/devnode-manager.js +546 -0
- package/dist/devnode-manager.js.map +1 -0
- package/dist/execution-key-cache.d.ts +71 -0
- package/dist/execution-key-cache.d.ts.map +1 -0
- package/dist/execution-key-cache.js +286 -0
- package/dist/execution-key-cache.js.map +1 -0
- package/dist/file-io.d.ts +2 -0
- package/dist/file-io.d.ts.map +1 -0
- package/dist/file-io.js +9 -0
- package/dist/file-io.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/named-account-manager.d.ts +40 -0
- package/dist/named-account-manager.d.ts.map +1 -0
- package/dist/named-account-manager.js +116 -0
- package/dist/named-account-manager.js.map +1 -0
- package/dist/network-manager.d.ts +36 -0
- package/dist/network-manager.d.ts.map +1 -0
- package/dist/network-manager.js +226 -0
- package/dist/network-manager.js.map +1 -0
- package/dist/sdk-adapter.d.ts +271 -0
- package/dist/sdk-adapter.d.ts.map +1 -0
- package/dist/sdk-adapter.js +997 -0
- package/dist/sdk-adapter.js.map +1 -0
- package/dist/sdk-diagnostics.d.ts +113 -0
- package/dist/sdk-diagnostics.d.ts.map +1 -0
- package/dist/sdk-diagnostics.js +241 -0
- package/dist/sdk-diagnostics.js.map +1 -0
- package/dist/transition-selector.d.ts +15 -0
- package/dist/transition-selector.d.ts.map +1 -0
- package/dist/transition-selector.js +45 -0
- package/dist/transition-selector.js.map +1 -0
- package/dist/types.d.ts +484 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +129 -0
- package/dist/types.js.map +1 -0
- package/package.json +35 -0
|
@@ -0,0 +1,997 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK adapter — isolates the @provablehq/sdk initialization ceremony.
|
|
3
|
+
*
|
|
4
|
+
* The Provable SDK v0.11.3 baseline requires:
|
|
5
|
+
* 1. initThreadPool() for multi-threaded WASM
|
|
6
|
+
* 2. Network-specific loading via @provablehq/sdk/dynamic.js
|
|
7
|
+
* 3. getOrInitConsensusVersionTestHeights() for devnode connections
|
|
8
|
+
* 4. ProgramManager with devnode-specific transaction builders
|
|
9
|
+
*
|
|
10
|
+
* This adapter loads the runtime SDK module dynamically per network while
|
|
11
|
+
* keeping TypeScript types anchored to the testnet entrypoint, which matches
|
|
12
|
+
* the mainnet surface in the current SDK release.
|
|
13
|
+
*/
|
|
14
|
+
import * as crypto from "node:crypto";
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { CREDITS_KEY_CACHE_FORMAT, fingerprintBytes, fingerprintFile, fingerprintsEqual, readCreditsKeyCacheMetadata, writeCreditsKeyCacheMetadata, } from "@lionden/core";
|
|
19
|
+
import { Address } from "@provablehq/sdk/testnet.js";
|
|
20
|
+
import { writeFileAtomic } from "./file-io.js";
|
|
21
|
+
import { SdkDiagnostics } from "./sdk-diagnostics.js";
|
|
22
|
+
/**
|
|
23
|
+
* Cache of derived program addresses. Derivation is a pure function of the
|
|
24
|
+
* program id and allocates+frees a WASM `Address`, so memoize per id.
|
|
25
|
+
*/
|
|
26
|
+
const programAddressCache = new Map();
|
|
27
|
+
export function programAddressFromProgramId(programId) {
|
|
28
|
+
const cached = programAddressCache.get(programId);
|
|
29
|
+
if (cached !== undefined) {
|
|
30
|
+
return cached;
|
|
31
|
+
}
|
|
32
|
+
const address = Address.fromProgramId(programId);
|
|
33
|
+
try {
|
|
34
|
+
const derived = address.to_string();
|
|
35
|
+
programAddressCache.set(programId, derived);
|
|
36
|
+
return derived;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
address.free();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function computeProgramChecksum(network, program) {
|
|
43
|
+
const sdk = await loadSdkModule(network);
|
|
44
|
+
const checksum = sdk
|
|
45
|
+
.programChecksum;
|
|
46
|
+
if (typeof checksum !== "function") {
|
|
47
|
+
throw new Error("Installed @provablehq/sdk does not expose programChecksum().");
|
|
48
|
+
}
|
|
49
|
+
return checksum(program);
|
|
50
|
+
}
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// SDK initialization
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
const SDK_VERSION = "^0.11.3";
|
|
55
|
+
let sdkInitPromise;
|
|
56
|
+
const sdkModuleCache = new Map();
|
|
57
|
+
const requireFromHere = createRequire(import.meta.url);
|
|
58
|
+
function normalizeSdkNetwork(network) {
|
|
59
|
+
switch (network) {
|
|
60
|
+
case "mainnet":
|
|
61
|
+
return "mainnet";
|
|
62
|
+
case "testnet":
|
|
63
|
+
case "canary":
|
|
64
|
+
return "testnet";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function loadSdkModule(network) {
|
|
68
|
+
const cached = sdkModuleCache.get(network);
|
|
69
|
+
if (cached) {
|
|
70
|
+
return cached;
|
|
71
|
+
}
|
|
72
|
+
const modulePromise = (async () => {
|
|
73
|
+
const { loadNetwork } = await import("@provablehq/sdk/dynamic.js");
|
|
74
|
+
return (await loadNetwork(normalizeSdkNetwork(network)));
|
|
75
|
+
})();
|
|
76
|
+
sdkModuleCache.set(network, modulePromise);
|
|
77
|
+
try {
|
|
78
|
+
return await modulePromise;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
sdkModuleCache.delete(network);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Initialize the Provable SDK WASM runtime.
|
|
87
|
+
* Must be called once before any SDK operations.
|
|
88
|
+
*/
|
|
89
|
+
export async function initSdk() {
|
|
90
|
+
if (!sdkInitPromise) {
|
|
91
|
+
sdkInitPromise = (async () => {
|
|
92
|
+
const sdk = await loadSdkModule("testnet");
|
|
93
|
+
if (typeof sdk.initThreadPool === "function") {
|
|
94
|
+
await sdk.initThreadPool();
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
await sdkInitPromise;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
sdkInitPromise = undefined;
|
|
103
|
+
throw new Error(`Failed to initialize @provablehq/sdk. ` +
|
|
104
|
+
`Ensure @provablehq/sdk@${SDK_VERSION} is installed.\n` +
|
|
105
|
+
`Original error: ${err instanceof Error ? err.message : String(err)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function applySdkLogLevel(sdk, logLevel = "warn") {
|
|
109
|
+
const setLogLevel = sdk.setLogLevel;
|
|
110
|
+
if (typeof setLogLevel === "function") {
|
|
111
|
+
setLogLevel(logLevel);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Egress policy
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/**
|
|
118
|
+
* Hosts the bundled @provablehq/sdk + WASM can fetch proving parameters and
|
|
119
|
+
* SRS files from. Hardcoded because (a) the SDK's URLs are baked into the
|
|
120
|
+
* WASM, (b) the artifact set is static, and (c) per-network user
|
|
121
|
+
* configuration of this list never delivered a meaningful hermeticity
|
|
122
|
+
* guarantee — true offline / hermetic operation requires a warmed cache
|
|
123
|
+
* plus external network isolation (CI / container / firewall). If a future
|
|
124
|
+
* SDK version adds a new host, update this list.
|
|
125
|
+
*/
|
|
126
|
+
const KNOWN_SDK_PARAMETER_HOSTS = new Set([
|
|
127
|
+
"parameters.provable.com",
|
|
128
|
+
"s3.us-west-1.amazonaws.com",
|
|
129
|
+
"parameters.aleo.org",
|
|
130
|
+
]);
|
|
131
|
+
function urlOf(input) {
|
|
132
|
+
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
133
|
+
return new URL(input.url);
|
|
134
|
+
}
|
|
135
|
+
if (input instanceof URL)
|
|
136
|
+
return input;
|
|
137
|
+
return new URL(String(input));
|
|
138
|
+
}
|
|
139
|
+
const GUARDED_TRANSPORT_REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
140
|
+
const GUARDED_TRANSPORT_MAX_REDIRECTS = 20;
|
|
141
|
+
function redirectAwareInit(init) {
|
|
142
|
+
// Force manual redirects so the guard can validate every Location before fetch follows it.
|
|
143
|
+
return { ...init, redirect: "manual" };
|
|
144
|
+
}
|
|
145
|
+
function nextRedirectInit(init, status) {
|
|
146
|
+
const method = init?.method?.toUpperCase();
|
|
147
|
+
const shouldSwitchToGet = (status === 303 && method !== "GET" && method !== "HEAD") ||
|
|
148
|
+
((status === 301 || status === 302) && method === "POST");
|
|
149
|
+
if (shouldSwitchToGet) {
|
|
150
|
+
const next = { ...init, method: "GET" };
|
|
151
|
+
delete next.body;
|
|
152
|
+
if (next.headers) {
|
|
153
|
+
const headers = new Headers(next.headers);
|
|
154
|
+
headers.delete("content-encoding");
|
|
155
|
+
headers.delete("content-length");
|
|
156
|
+
headers.delete("content-type");
|
|
157
|
+
next.headers = headers;
|
|
158
|
+
}
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
return init;
|
|
162
|
+
}
|
|
163
|
+
async function fetchWithEgressGuardedRedirects(input, init, validateUrl) {
|
|
164
|
+
let currentUrl;
|
|
165
|
+
try {
|
|
166
|
+
currentUrl = urlOf(input);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return fetch(input, redirectAwareInit(init));
|
|
170
|
+
}
|
|
171
|
+
let currentInput = input;
|
|
172
|
+
let currentInit = init;
|
|
173
|
+
let redirectCount = 0;
|
|
174
|
+
const visited = new Set();
|
|
175
|
+
for (;;) {
|
|
176
|
+
validateUrl(currentUrl);
|
|
177
|
+
if (visited.has(currentUrl.href)) {
|
|
178
|
+
throw new Error(`LionDen SDK transport detected redirect loop to "${currentUrl.href}".`);
|
|
179
|
+
}
|
|
180
|
+
visited.add(currentUrl.href);
|
|
181
|
+
const response = await fetch(currentInput, redirectAwareInit(currentInit));
|
|
182
|
+
if (!GUARDED_TRANSPORT_REDIRECT_STATUSES.has(response.status)) {
|
|
183
|
+
return response;
|
|
184
|
+
}
|
|
185
|
+
const location = response.headers.get("location");
|
|
186
|
+
if (!location) {
|
|
187
|
+
return response;
|
|
188
|
+
}
|
|
189
|
+
if (redirectCount >= GUARDED_TRANSPORT_MAX_REDIRECTS) {
|
|
190
|
+
throw new Error(`LionDen SDK transport exceeded ${GUARDED_TRANSPORT_MAX_REDIRECTS} redirects.`);
|
|
191
|
+
}
|
|
192
|
+
const nextUrl = new URL(location, currentUrl);
|
|
193
|
+
redirectCount += 1;
|
|
194
|
+
currentUrl = nextUrl;
|
|
195
|
+
currentInput = nextUrl;
|
|
196
|
+
currentInit = nextRedirectInit(currentInit, response.status);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function validateNetworkHost(url, allowed, violation) {
|
|
200
|
+
if (!allowed.has(url.host)) {
|
|
201
|
+
const msg = `LionDen blocked SDK network fetch to host "${url.host}". ` +
|
|
202
|
+
`Allowed hosts: ${allowed.size === 0 ? "(none)" : Array.from(allowed).join(", ")}. Extend sdk.egress.networkHosts or change sdk.egress.violation.`;
|
|
203
|
+
if (violation === "block") {
|
|
204
|
+
throw new Error(msg);
|
|
205
|
+
}
|
|
206
|
+
console.warn(msg);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Build a `fetch`-shaped transport for SDK **network-host** calls
|
|
211
|
+
* (chain state, transaction submission). On a host outside `allowed`,
|
|
212
|
+
* rejects when `violation === "block"` and logs-then-forwards when
|
|
213
|
+
* `violation === "warn"`. Warn mode forwards the SDK request unchanged,
|
|
214
|
+
* including headers, so use it only when intentionally observing egress
|
|
215
|
+
* instead of enforcing it. Exported for unit testing.
|
|
216
|
+
*
|
|
217
|
+
* When `diagnostics` is supplied, the transport records non-OK responses
|
|
218
|
+
* (status/statusText/body excerpt) and thrown fetch/host-block failures so
|
|
219
|
+
* that `captureSdkCall` can surface the underlying state-query failure behind
|
|
220
|
+
* an opaque WASM error. Without it (the 2-arg form), behavior is unchanged.
|
|
221
|
+
*/
|
|
222
|
+
export function makeNetworkTransport(allowed, violation, diagnostics) {
|
|
223
|
+
if (diagnostics === undefined) {
|
|
224
|
+
return (input, init) => fetchWithEgressGuardedRedirects(input, init, (url) => validateNetworkHost(url, allowed, violation));
|
|
225
|
+
}
|
|
226
|
+
return async (input, init) => {
|
|
227
|
+
const method = init?.method?.toUpperCase() ?? "GET";
|
|
228
|
+
let url;
|
|
229
|
+
try {
|
|
230
|
+
url = urlOf(input).href;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
url = String(input);
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const response = await fetchWithEgressGuardedRedirects(input, init, (u) => validateNetworkHost(u, allowed, violation));
|
|
237
|
+
if (!response.ok) {
|
|
238
|
+
let bodyExcerpt;
|
|
239
|
+
try {
|
|
240
|
+
// clone() so the SDK still gets to read the original body.
|
|
241
|
+
bodyExcerpt = (await response.clone().text()).slice(0, 512);
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Body-read failures are non-fatal diagnostics — ignore.
|
|
245
|
+
}
|
|
246
|
+
diagnostics.record({
|
|
247
|
+
method,
|
|
248
|
+
url,
|
|
249
|
+
status: response.status,
|
|
250
|
+
statusText: response.statusText,
|
|
251
|
+
...(bodyExcerpt === undefined ? {} : { bodyExcerpt }),
|
|
252
|
+
at: Date.now(),
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return response;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
diagnostics.record({
|
|
259
|
+
method,
|
|
260
|
+
url,
|
|
261
|
+
error: error instanceof Error ? error.message : String(error),
|
|
262
|
+
at: Date.now(),
|
|
263
|
+
});
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Build a `fetch`-shaped transport for SDK **parameter-host** calls
|
|
270
|
+
* (credits proving/verifying keys, SRS files). Allowlist is the
|
|
271
|
+
* module-private `KNOWN_SDK_PARAMETER_HOSTS`; violation is always block.
|
|
272
|
+
* An unknown host means LionDen's allowlist is stale relative to the
|
|
273
|
+
* installed SDK, surfaced as a clear actionable error. Exported for
|
|
274
|
+
* unit testing.
|
|
275
|
+
*/
|
|
276
|
+
export function makeParameterTransport() {
|
|
277
|
+
return async (input, init) => {
|
|
278
|
+
let primaryUrl;
|
|
279
|
+
try {
|
|
280
|
+
primaryUrl = urlOf(input);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return fetchParameterUrl(input, init);
|
|
284
|
+
}
|
|
285
|
+
const mirrorUrl = parameterMirrorUrl(primaryUrl);
|
|
286
|
+
try {
|
|
287
|
+
const response = await fetchParameterUrl(input, init);
|
|
288
|
+
if (response.ok || !mirrorUrl)
|
|
289
|
+
return response;
|
|
290
|
+
const mirrorResponse = await tryParameterMirror(mirrorUrl, init);
|
|
291
|
+
return mirrorResponse?.ok ? mirrorResponse : response;
|
|
292
|
+
}
|
|
293
|
+
catch (primaryError) {
|
|
294
|
+
if (primaryError instanceof ParameterHostValidationError)
|
|
295
|
+
throw primaryError;
|
|
296
|
+
if (!mirrorUrl)
|
|
297
|
+
throw primaryError;
|
|
298
|
+
const mirrorResponse = await tryParameterMirror(mirrorUrl, init);
|
|
299
|
+
if (mirrorResponse?.ok)
|
|
300
|
+
return mirrorResponse;
|
|
301
|
+
throw primaryError;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
class ParameterHostValidationError extends Error {
|
|
306
|
+
constructor(message) {
|
|
307
|
+
super(message);
|
|
308
|
+
this.name = "ParameterHostValidationError";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function tryParameterMirror(mirrorUrl, init) {
|
|
312
|
+
try {
|
|
313
|
+
// SDK parameter downloads are GET-style requests; reuse the same init so
|
|
314
|
+
// headers/options stay identical between primary and mirror attempts.
|
|
315
|
+
return await fetchParameterUrl(mirrorUrl, init);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Keep the primary host failure as the diagnostic the SDK reports.
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function fetchParameterUrl(input, init) {
|
|
323
|
+
return fetchWithEgressGuardedRedirects(input, init, validateParameterHost);
|
|
324
|
+
}
|
|
325
|
+
function validateParameterHost(url) {
|
|
326
|
+
if (!KNOWN_SDK_PARAMETER_HOSTS.has(url.host)) {
|
|
327
|
+
const msg = `LionDen does not recognize SDK parameter host "${url.host}". ` +
|
|
328
|
+
`Known hosts: ${Array.from(KNOWN_SDK_PARAMETER_HOSTS).join(", ")}. ` +
|
|
329
|
+
`This may indicate a stale LionDen allowlist; please report.`;
|
|
330
|
+
throw new ParameterHostValidationError(msg);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function parameterMirrorUrl(url) {
|
|
334
|
+
if (url.protocol !== "https:" || url.host !== "parameters.provable.com") {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const match = url.pathname.match(/^\/([^/]+)\/(.+)$/);
|
|
338
|
+
if (!match)
|
|
339
|
+
return undefined;
|
|
340
|
+
const [, network, artifactPath] = match;
|
|
341
|
+
return new URL(`https://s3.us-west-1.amazonaws.com/${network}.parameters/${artifactPath}${url.search}`);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Create SDK objects for a given network and endpoint.
|
|
345
|
+
* Validates that required devnode methods exist (version guard).
|
|
346
|
+
*/
|
|
347
|
+
export async function createSdkObjects(opts) {
|
|
348
|
+
assertValidEndpoint(opts.endpoint, "createSdkObjects");
|
|
349
|
+
await initSdk();
|
|
350
|
+
try {
|
|
351
|
+
const sdk = await loadSdkModule(opts.network);
|
|
352
|
+
applySdkLogLevel(sdk, opts.logLevel);
|
|
353
|
+
const { Account, AleoNetworkClient, AleoKeyProvider, LocalFileKeyStore, NetworkRecordProvider, ProgramManager, ProgramManagerBase, } = sdk;
|
|
354
|
+
// Create account
|
|
355
|
+
const account = opts.privateKey ? new Account({ privateKey: opts.privateKey }) : new Account();
|
|
356
|
+
// Per-bundle transport-failure sink. Shared by every AleoNetworkClient
|
|
357
|
+
// built here (default + PM-internal) so state-query failures observed
|
|
358
|
+
// during prove are attributable to this connection's executions.
|
|
359
|
+
const diagnostics = new SdkDiagnostics();
|
|
360
|
+
// Network transport (chain state, transaction submission). Installing
|
|
361
|
+
// the transport on AleoNetworkClient flips hasCustomTransport=true in
|
|
362
|
+
// the SDK, forcing the prove path to use CallbackQuery instead of
|
|
363
|
+
// WASM's internal SnapshotQuery (closes the statePaths leak).
|
|
364
|
+
// networkClientOptions is also passed to ProgramManager below so its
|
|
365
|
+
// internal AleoNetworkClient inherits the same transport.
|
|
366
|
+
const networkTransport = makeNetworkTransport(opts.egressPolicy.allowedNetworkHosts, opts.egressPolicy.violation, diagnostics);
|
|
367
|
+
const networkClientOptions = {
|
|
368
|
+
transport: networkTransport,
|
|
369
|
+
};
|
|
370
|
+
if (opts.apiKey) {
|
|
371
|
+
networkClientOptions.headers = { Authorization: `Bearer ${opts.apiKey}` };
|
|
372
|
+
}
|
|
373
|
+
const networkClient = new AleoNetworkClient(opts.endpoint, networkClientOptions);
|
|
374
|
+
// Parameter transport (credits proving/verifying keys, SRS files).
|
|
375
|
+
// Always installed — allowlist is an internal LionDen invariant
|
|
376
|
+
// (KNOWN_SDK_PARAMETER_HOSTS) independent of egressPolicy.
|
|
377
|
+
const keyProvider = new AleoKeyProvider({ transport: makeParameterTransport() });
|
|
378
|
+
keyProvider.useCache(true);
|
|
379
|
+
let effectiveKeyProvider = keyProvider;
|
|
380
|
+
if (opts.keyCache?.storage === "filesystem" && opts.keyCache.path) {
|
|
381
|
+
const cachePath = opts.keyCache.path;
|
|
382
|
+
const { wasmHash } = getSdkRuntimeMetadata(opts.network);
|
|
383
|
+
await warmupCreditsKeys(keyProvider, sdk, cachePath, opts.network, wasmHash);
|
|
384
|
+
effectiveKeyProvider = new PersistentFunctionKeyProvider(keyProvider, new LocalFileKeyStore(cachePath), { sdk, cachePath, network: opts.network, wasmHash });
|
|
385
|
+
}
|
|
386
|
+
const recordProvider = new NetworkRecordProvider(account, networkClient);
|
|
387
|
+
// Create program manager — pass networkClientOptions so the PM's internal
|
|
388
|
+
// network client inherits API key headers for authenticated endpoints.
|
|
389
|
+
const programManager = new ProgramManager(opts.endpoint, effectiveKeyProvider, recordProvider, networkClientOptions);
|
|
390
|
+
programManager.setAccount(account);
|
|
391
|
+
return {
|
|
392
|
+
account,
|
|
393
|
+
networkClient,
|
|
394
|
+
programManager,
|
|
395
|
+
programManagerBase: ProgramManagerBase,
|
|
396
|
+
keyProvider: effectiveKeyProvider,
|
|
397
|
+
recordProvider,
|
|
398
|
+
diagnostics,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
throw new Error(`Failed to create SDK objects for network "${opts.network}" at ${opts.endpoint}. ` +
|
|
403
|
+
`Ensure @provablehq/sdk@${SDK_VERSION} is installed.\n` +
|
|
404
|
+
`Original error: ${err instanceof Error ? err.message : String(err)}`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Resolve which credits.aleo entry (if any) a `functionKeys()` call
|
|
409
|
+
* refers to. The SDK reaches some entries — notably `set_validator_state`
|
|
410
|
+
* — only through the generic `functionKeys()` path with one of:
|
|
411
|
+
* - `{ name: "<credits_entry>" }`
|
|
412
|
+
* - `{ cacheKey: "credits.aleo/<credits_entry>" }`
|
|
413
|
+
* - `{ proverUri, verifierUri, cacheKey? }` where the URIs match an
|
|
414
|
+
* entry's prover/verifier in `CREDITS_PROGRAM_KEYS`.
|
|
415
|
+
* Returns the matching entry name, or `undefined` if the params do not
|
|
416
|
+
* identify a known credits entry. Non-credits user keys are intentionally
|
|
417
|
+
* left unpersisted (LionDen does not own arbitrary-locator persistence).
|
|
418
|
+
*/
|
|
419
|
+
function creditsEntryFromFunctionKeysParams(sdk, params) {
|
|
420
|
+
if (!params || typeof params !== "object")
|
|
421
|
+
return undefined;
|
|
422
|
+
const credits = sdk.CREDITS_PROGRAM_KEYS;
|
|
423
|
+
const p = params;
|
|
424
|
+
const name = typeof p.name === "string" ? p.name : undefined;
|
|
425
|
+
if (name && Object.hasOwn(credits, name) && name !== "getKey") {
|
|
426
|
+
return name;
|
|
427
|
+
}
|
|
428
|
+
const cacheKey = typeof p.cacheKey === "string" ? p.cacheKey : undefined;
|
|
429
|
+
if (cacheKey) {
|
|
430
|
+
const prefix = "credits.aleo/";
|
|
431
|
+
if (cacheKey.startsWith(prefix)) {
|
|
432
|
+
const entry = cacheKey.slice(prefix.length);
|
|
433
|
+
if (Object.hasOwn(credits, entry) && entry !== "getKey") {
|
|
434
|
+
return entry;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const proverUri = typeof p.proverUri === "string" ? p.proverUri : undefined;
|
|
439
|
+
if (proverUri) {
|
|
440
|
+
for (const [entry, value] of Object.entries(credits)) {
|
|
441
|
+
if (entry === "getKey")
|
|
442
|
+
continue;
|
|
443
|
+
if (value && typeof value === "object" && value.prover === proverUri) {
|
|
444
|
+
return entry;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return undefined;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Map a `transferKeys(visibility)` argument to the corresponding entry
|
|
452
|
+
* in CREDITS_PROGRAM_KEYS. Mirrors the SDK's visibility sets at
|
|
453
|
+
* `node_modules/@provablehq/sdk/dist/testnet/browser.js:2913-2929`.
|
|
454
|
+
* Unknown visibility strings fall through (returns `undefined`) so the
|
|
455
|
+
* delegate result is still returned to the caller; only persistence is
|
|
456
|
+
* skipped.
|
|
457
|
+
*/
|
|
458
|
+
function transferKeyNameForVisibility(visibility) {
|
|
459
|
+
switch (visibility) {
|
|
460
|
+
case "private":
|
|
461
|
+
case "transfer_private":
|
|
462
|
+
case "transferPrivate":
|
|
463
|
+
return "transfer_private";
|
|
464
|
+
case "private_to_public":
|
|
465
|
+
case "privateToPublic":
|
|
466
|
+
case "transfer_private_to_public":
|
|
467
|
+
case "transferPrivateToPublic":
|
|
468
|
+
return "transfer_private_to_public";
|
|
469
|
+
case "public":
|
|
470
|
+
case "transfer_public":
|
|
471
|
+
case "transferPublic":
|
|
472
|
+
return "transfer_public";
|
|
473
|
+
case "public_as_signer":
|
|
474
|
+
case "transfer_public_as_signer":
|
|
475
|
+
case "transferPublicAsSigner":
|
|
476
|
+
return "transfer_public_as_signer";
|
|
477
|
+
case "public_to_private":
|
|
478
|
+
case "publicToPrivate":
|
|
479
|
+
case "transfer_public_to_private":
|
|
480
|
+
case "transferPublicToPrivate":
|
|
481
|
+
return "transfer_public_to_private";
|
|
482
|
+
default:
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
export class PersistentFunctionKeyProvider {
|
|
487
|
+
delegate;
|
|
488
|
+
fileStore;
|
|
489
|
+
creditsPersistence;
|
|
490
|
+
constructor(delegate, fileStore, creditsPersistence) {
|
|
491
|
+
this.delegate = delegate;
|
|
492
|
+
this.fileStore = fileStore;
|
|
493
|
+
this.creditsPersistence = creditsPersistence;
|
|
494
|
+
}
|
|
495
|
+
async keyStore() {
|
|
496
|
+
return this.fileStore;
|
|
497
|
+
}
|
|
498
|
+
async bondPublicKeys() {
|
|
499
|
+
const keys = await this.delegate.bondPublicKeys();
|
|
500
|
+
this.persistCreditsIfMissing("bond_public", keys[0]);
|
|
501
|
+
return keys;
|
|
502
|
+
}
|
|
503
|
+
async bondValidatorKeys() {
|
|
504
|
+
const keys = await this.delegate.bondValidatorKeys();
|
|
505
|
+
this.persistCreditsIfMissing("bond_validator", keys[0]);
|
|
506
|
+
return keys;
|
|
507
|
+
}
|
|
508
|
+
cacheKeys(keyId, keys) {
|
|
509
|
+
this.delegate.cacheKeys(keyId, keys);
|
|
510
|
+
}
|
|
511
|
+
async claimUnbondPublicKeys() {
|
|
512
|
+
const keys = await this.delegate.claimUnbondPublicKeys();
|
|
513
|
+
this.persistCreditsIfMissing("claim_unbond_public", keys[0]);
|
|
514
|
+
return keys;
|
|
515
|
+
}
|
|
516
|
+
async functionKeys(params) {
|
|
517
|
+
const keys = await this.delegate.functionKeys(params);
|
|
518
|
+
// The SDK reaches some credits.aleo entries (notably `set_validator_state`)
|
|
519
|
+
// only through this generic path. Persist them by name when the params
|
|
520
|
+
// identify a known entry; non-credits user keys are left to the
|
|
521
|
+
// delegate's own in-memory cache.
|
|
522
|
+
const config = this.creditsPersistence;
|
|
523
|
+
if (config) {
|
|
524
|
+
const entry = creditsEntryFromFunctionKeysParams(config.sdk, params);
|
|
525
|
+
if (entry)
|
|
526
|
+
this.persistCreditsIfMissing(entry, keys[0]);
|
|
527
|
+
}
|
|
528
|
+
return keys;
|
|
529
|
+
}
|
|
530
|
+
async feePrivateKeys() {
|
|
531
|
+
const keys = await this.delegate.feePrivateKeys();
|
|
532
|
+
this.persistCreditsIfMissing("fee_private", keys[0]);
|
|
533
|
+
return keys;
|
|
534
|
+
}
|
|
535
|
+
async feePublicKeys() {
|
|
536
|
+
const keys = await this.delegate.feePublicKeys();
|
|
537
|
+
this.persistCreditsIfMissing("fee_public", keys[0]);
|
|
538
|
+
return keys;
|
|
539
|
+
}
|
|
540
|
+
async inclusionKeys() {
|
|
541
|
+
const keys = await this.delegate.inclusionKeys();
|
|
542
|
+
this.persistCreditsIfMissing("inclusion", keys[0]);
|
|
543
|
+
return keys;
|
|
544
|
+
}
|
|
545
|
+
async joinKeys() {
|
|
546
|
+
const keys = await this.delegate.joinKeys();
|
|
547
|
+
this.persistCreditsIfMissing("join", keys[0]);
|
|
548
|
+
return keys;
|
|
549
|
+
}
|
|
550
|
+
async splitKeys() {
|
|
551
|
+
const keys = await this.delegate.splitKeys();
|
|
552
|
+
this.persistCreditsIfMissing("split", keys[0]);
|
|
553
|
+
return keys;
|
|
554
|
+
}
|
|
555
|
+
async transferKeys(visibility) {
|
|
556
|
+
const keys = await this.delegate.transferKeys(visibility);
|
|
557
|
+
const name = transferKeyNameForVisibility(visibility);
|
|
558
|
+
if (name !== undefined) {
|
|
559
|
+
this.persistCreditsIfMissing(name, keys[0]);
|
|
560
|
+
}
|
|
561
|
+
return keys;
|
|
562
|
+
}
|
|
563
|
+
async unBondPublicKeys() {
|
|
564
|
+
const keys = await this.delegate.unBondPublicKeys();
|
|
565
|
+
this.persistCreditsIfMissing("unbond_public", keys[0]);
|
|
566
|
+
return keys;
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Persist a credits.aleo proving key to disk so the next process can
|
|
570
|
+
* warm its in-memory cache without re-fetching from the public
|
|
571
|
+
* parameters host. Performance-only — hermeticity comes from the
|
|
572
|
+
* SDK egress policy, not from caching.
|
|
573
|
+
*/
|
|
574
|
+
persistCreditsIfMissing(name, provingKey) {
|
|
575
|
+
const config = this.creditsPersistence;
|
|
576
|
+
if (!config)
|
|
577
|
+
return;
|
|
578
|
+
try {
|
|
579
|
+
const credits = config.sdk.CREDITS_PROGRAM_KEYS;
|
|
580
|
+
const locator = credits[name]?.locator;
|
|
581
|
+
if (!locator)
|
|
582
|
+
return;
|
|
583
|
+
const paths = creditsCachePaths(config.cachePath, config.wasmHash, config.network, locator);
|
|
584
|
+
const bytes = keyToBytes(provingKey, "proving");
|
|
585
|
+
const fingerprint = fingerprintBytes(bytes);
|
|
586
|
+
// Skip the write only when the existing on-disk entry is complete and
|
|
587
|
+
// matches what we would write (both files present, metadata identity +
|
|
588
|
+
// fingerprint align). Anything torn or stale gets rewritten so warmup
|
|
589
|
+
// can pick it up on the next run.
|
|
590
|
+
if (isCreditsEntryCurrent(paths, locator, config, fingerprint))
|
|
591
|
+
return;
|
|
592
|
+
writeFileAtomic(paths.prover, bytes);
|
|
593
|
+
writeCreditsKeyCacheMetadata(paths.metadata, {
|
|
594
|
+
format: CREDITS_KEY_CACHE_FORMAT,
|
|
595
|
+
locator,
|
|
596
|
+
network: config.network,
|
|
597
|
+
wasmHash: config.wasmHash,
|
|
598
|
+
prover: fingerprint,
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
// Persistence is opportunistic — never block the caller.
|
|
603
|
+
console.debug(`LionDen: failed to persist credits.aleo/${name} proving key: ${err instanceof Error ? err.message : String(err)}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Warm the SDK's in-memory key cache from disk for every credits.aleo
|
|
609
|
+
* key that has a complete on-disk entry. Frame as performance only —
|
|
610
|
+
* hermeticity is enforced by the SDK egress policy, not the cache.
|
|
611
|
+
*
|
|
612
|
+
* Iterates every named entry of `CREDITS_PROGRAM_KEYS` (skipping the
|
|
613
|
+
* helper functions like `getKey`). Entries without a complete and
|
|
614
|
+
* fingerprint-matching cache entry are skipped silently.
|
|
615
|
+
*
|
|
616
|
+
* @internal — exported for testing.
|
|
617
|
+
*/
|
|
618
|
+
export async function warmupCreditsKeys(keyProvider, sdk, cachePath, network, wasmHash) {
|
|
619
|
+
const credits = sdk.CREDITS_PROGRAM_KEYS;
|
|
620
|
+
for (const [name, value] of Object.entries(credits)) {
|
|
621
|
+
if (!isWarmableCreditsEntry(value))
|
|
622
|
+
continue;
|
|
623
|
+
const key = value;
|
|
624
|
+
try {
|
|
625
|
+
const paths = creditsCachePaths(cachePath, wasmHash, network, key.locator);
|
|
626
|
+
if (!fs.existsSync(paths.prover) || !fs.existsSync(paths.metadata))
|
|
627
|
+
continue;
|
|
628
|
+
const metadata = readCreditsKeyCacheMetadata(paths.metadata);
|
|
629
|
+
if (!metadata)
|
|
630
|
+
continue;
|
|
631
|
+
if (metadata.locator !== key.locator)
|
|
632
|
+
continue;
|
|
633
|
+
if (metadata.network !== network)
|
|
634
|
+
continue;
|
|
635
|
+
if (metadata.wasmHash !== wasmHash)
|
|
636
|
+
continue;
|
|
637
|
+
const bytes = new Uint8Array(fs.readFileSync(paths.prover));
|
|
638
|
+
if (!fingerprintsEqual(fingerprintBytes(bytes), metadata.prover))
|
|
639
|
+
continue;
|
|
640
|
+
const provingKey = sdk.ProvingKey.fromBytes(bytes);
|
|
641
|
+
const verifyingKey = key.verifyingKey();
|
|
642
|
+
keyProvider.cacheKeys(key.locator, [provingKey, verifyingKey]);
|
|
643
|
+
}
|
|
644
|
+
catch (err) {
|
|
645
|
+
console.debug(`LionDen: skipping credits.aleo/${name} warmup: ${err instanceof Error ? err.message : String(err)}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function isWarmableCreditsEntry(value) {
|
|
650
|
+
return (typeof value === "object" &&
|
|
651
|
+
value !== null &&
|
|
652
|
+
typeof value.locator === "string" &&
|
|
653
|
+
typeof value.verifyingKey === "function");
|
|
654
|
+
}
|
|
655
|
+
function isCreditsEntryCurrent(paths, locator, config, fingerprint) {
|
|
656
|
+
if (!fs.existsSync(paths.prover) || !fs.existsSync(paths.metadata))
|
|
657
|
+
return false;
|
|
658
|
+
let metadata;
|
|
659
|
+
try {
|
|
660
|
+
metadata = readCreditsKeyCacheMetadata(paths.metadata);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
if (!metadata)
|
|
666
|
+
return false;
|
|
667
|
+
if (metadata.locator !== locator)
|
|
668
|
+
return false;
|
|
669
|
+
if (metadata.network !== config.network)
|
|
670
|
+
return false;
|
|
671
|
+
if (metadata.wasmHash !== config.wasmHash)
|
|
672
|
+
return false;
|
|
673
|
+
if (!fingerprintsEqual(metadata.prover, fingerprint))
|
|
674
|
+
return false;
|
|
675
|
+
// Confirm the on-disk prover bytes actually match the metadata claim.
|
|
676
|
+
// Otherwise a corrupted .prover with intact metadata sneaks past write-back
|
|
677
|
+
// and gets rejected by warmup on the next run, leaving a permanently cold
|
|
678
|
+
// entry.
|
|
679
|
+
let onDisk;
|
|
680
|
+
try {
|
|
681
|
+
onDisk = fingerprintFile(paths.prover);
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
return fingerprintsEqual(onDisk, fingerprint);
|
|
687
|
+
}
|
|
688
|
+
function creditsCachePaths(cachePath, wasmHash, network, locator) {
|
|
689
|
+
// `locator` includes `/` and `:`; base64url avoids any filesystem-hostile chars.
|
|
690
|
+
const safeLocator = Buffer.from(locator, "utf-8").toString("base64url");
|
|
691
|
+
const dir = path.join(cachePath, "lionden-credits", wasmHash, network);
|
|
692
|
+
return {
|
|
693
|
+
dir,
|
|
694
|
+
prover: path.join(dir, `${safeLocator}.prover`),
|
|
695
|
+
metadata: path.join(dir, `${safeLocator}.metadata.json`),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
export async function createExecutionKeysFromBytes(network, keyBytes) {
|
|
699
|
+
await initSdk();
|
|
700
|
+
const sdk = await loadSdkModule(network);
|
|
701
|
+
return {
|
|
702
|
+
provingKey: sdk.ProvingKey.fromBytes(new Uint8Array(keyBytes.provingKey)),
|
|
703
|
+
verifyingKey: sdk.VerifyingKey.fromBytes(new Uint8Array(keyBytes.verifyingKey)),
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
export async function synthesizeExecutionKeyBytes(options) {
|
|
707
|
+
// Retained for future query-bound synthesis or explicit diagnostics. Runtime
|
|
708
|
+
// execution no longer calls this on cache misses because the SDK eager
|
|
709
|
+
// synthesis path cannot receive LionDen's guarded query object.
|
|
710
|
+
const keyPair = await options.programManagerBase.synthesizeKeyPair(options.privateKey, options.source, options.transitionName, [...options.inputs], options.imports, options.edition);
|
|
711
|
+
const provingKey = keyPair.provingKey();
|
|
712
|
+
const verifyingKey = keyPair.verifyingKey();
|
|
713
|
+
return {
|
|
714
|
+
provingKeyBytes: keyToBytes(provingKey, "proving"),
|
|
715
|
+
verifyingKeyBytes: keyToBytes(verifyingKey, "verifying"),
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
export function getSdkRuntimeMetadata(network) {
|
|
719
|
+
const wasmPath = resolveWasmArtifactPath(network);
|
|
720
|
+
return {
|
|
721
|
+
sdkVersion: readPackageVersion(resolvePackageRoot("@provablehq/sdk/testnet.js")),
|
|
722
|
+
wasmVersion: readPackageVersion(resolvePackageRoot(`@provablehq/wasm/${normalizeSdkNetwork(network)}.js`)),
|
|
723
|
+
wasmHash: crypto.createHash("sha256").update(fs.readFileSync(wasmPath)).digest("hex"),
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Create an isolated set of SDK objects for a specific signer.
|
|
728
|
+
* Reuses the shared KeyProvider (proving-key cache) but creates
|
|
729
|
+
* dedicated Account, RecordProvider, and ProgramManager instances.
|
|
730
|
+
*/
|
|
731
|
+
export async function createSignerSdkObjects(opts) {
|
|
732
|
+
assertValidEndpoint(opts.endpoint, "createSignerSdkObjects");
|
|
733
|
+
await initSdk();
|
|
734
|
+
const sdk = await loadSdkModule(opts.network);
|
|
735
|
+
applySdkLogLevel(sdk, opts.logLevel);
|
|
736
|
+
const { Account, AleoNetworkClient, NetworkRecordProvider, ProgramManager, ProgramManagerBase } = sdk;
|
|
737
|
+
const account = new Account({ privateKey: opts.privateKey });
|
|
738
|
+
// Per-signer transport-failure sink. State queries during a signer
|
|
739
|
+
// execution go through this signer PM's own transport, so it needs its
|
|
740
|
+
// own diagnostics sink (distinct from the default connection's).
|
|
741
|
+
const diagnostics = new SdkDiagnostics();
|
|
742
|
+
// Dedicated NetworkClient with API key for record lookups. Install the
|
|
743
|
+
// same guarded network transport that createSdkObjects uses so the
|
|
744
|
+
// per-signer PM's internal AleoNetworkClient also reports
|
|
745
|
+
// hasCustomTransport=true and routes prove-time state queries through
|
|
746
|
+
// JS. The signer path reuses the default connection's keyProvider, so
|
|
747
|
+
// no parameter transport is installed here.
|
|
748
|
+
const networkTransport = makeNetworkTransport(opts.egressPolicy.allowedNetworkHosts, opts.egressPolicy.violation, diagnostics);
|
|
749
|
+
const ncOptions = {
|
|
750
|
+
transport: networkTransport,
|
|
751
|
+
};
|
|
752
|
+
if (opts.apiKey)
|
|
753
|
+
ncOptions.headers = { Authorization: `Bearer ${opts.apiKey}` };
|
|
754
|
+
const networkClient = new AleoNetworkClient(opts.endpoint, ncOptions);
|
|
755
|
+
const recordProvider = new NetworkRecordProvider(account, networkClient);
|
|
756
|
+
const programManager = new ProgramManager(opts.endpoint, opts.keyProvider, recordProvider, ncOptions);
|
|
757
|
+
programManager.setAccount(account);
|
|
758
|
+
return {
|
|
759
|
+
account,
|
|
760
|
+
recordProvider,
|
|
761
|
+
programManager,
|
|
762
|
+
programManagerBase: ProgramManagerBase,
|
|
763
|
+
diagnostics,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function assertValidEndpoint(endpoint, context) {
|
|
767
|
+
if (typeof endpoint !== "string" || endpoint.length === 0) {
|
|
768
|
+
throw new Error(`${context} requires a non-empty endpoint string.`);
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
new URL(endpoint);
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
throw new Error(`${context}: invalid endpoint URL "${endpoint}".`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Version guard: check that the SDK has devnode-specific methods.
|
|
779
|
+
* Call this before attempting devnode operations.
|
|
780
|
+
*/
|
|
781
|
+
export async function checkDevnodeSdkSupport() {
|
|
782
|
+
await initSdk();
|
|
783
|
+
try {
|
|
784
|
+
const sdk = await loadSdkModule("testnet");
|
|
785
|
+
const { ProgramManager } = sdk;
|
|
786
|
+
const requiredMethods = [
|
|
787
|
+
"buildDevnodeExecutionTransaction",
|
|
788
|
+
"buildDevnodeDeploymentTransaction",
|
|
789
|
+
"buildDevnodeUpgradeTransaction",
|
|
790
|
+
];
|
|
791
|
+
const programManagerPrototype = ProgramManager.prototype;
|
|
792
|
+
for (const method of requiredMethods) {
|
|
793
|
+
if (typeof programManagerPrototype[method] !== "function") {
|
|
794
|
+
throw new Error(`ProgramManager is missing method "${method}". ` +
|
|
795
|
+
`This method requires @provablehq/sdk@${SDK_VERSION}. ` +
|
|
796
|
+
`Your installed version may be too old.`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
if (err instanceof Error && err.message.includes("ProgramManager")) {
|
|
802
|
+
throw err;
|
|
803
|
+
}
|
|
804
|
+
throw new Error(`Failed to verify SDK devnode support. ` +
|
|
805
|
+
`Ensure @provablehq/sdk@${SDK_VERSION} is installed.\n` +
|
|
806
|
+
`Original error: ${err instanceof Error ? err.message : String(err)}`);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
function keyToBytes(key, label) {
|
|
810
|
+
if (!key || typeof key.toBytes !== "function") {
|
|
811
|
+
throw new Error(`Synthesized ${label} key does not expose toBytes().`);
|
|
812
|
+
}
|
|
813
|
+
const raw = key.toBytes();
|
|
814
|
+
if (raw instanceof Uint8Array) {
|
|
815
|
+
return new Uint8Array(raw);
|
|
816
|
+
}
|
|
817
|
+
if (raw instanceof ArrayBuffer) {
|
|
818
|
+
return new Uint8Array(raw);
|
|
819
|
+
}
|
|
820
|
+
if (Array.isArray(raw)) {
|
|
821
|
+
return new Uint8Array(raw);
|
|
822
|
+
}
|
|
823
|
+
throw new Error(`Synthesized ${label} key bytes are not Uint8Array-compatible.`);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Initialize consensus version test heights for devnode connections.
|
|
827
|
+
* Required by the SDK before devnode transaction builders can be used.
|
|
828
|
+
*/
|
|
829
|
+
export async function initConsensusHeights() {
|
|
830
|
+
try {
|
|
831
|
+
const sdk = await loadSdkModule("testnet");
|
|
832
|
+
if (typeof sdk.getOrInitConsensusVersionTestHeights === "function") {
|
|
833
|
+
// TOOD: support custom heights
|
|
834
|
+
sdk.getOrInitConsensusVersionTestHeights();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
catch {
|
|
838
|
+
// Non-fatal — may not be needed for all operations
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function resolveWasmArtifactPath(network) {
|
|
842
|
+
const modulePath = requireFromHere.resolve(`@provablehq/wasm/${normalizeSdkNetwork(network)}.js`);
|
|
843
|
+
return path.join(path.dirname(modulePath), "aleo_wasm.wasm");
|
|
844
|
+
}
|
|
845
|
+
function resolvePackageRoot(specifier) {
|
|
846
|
+
try {
|
|
847
|
+
let current = path.dirname(requireFromHere.resolve(specifier));
|
|
848
|
+
while (true) {
|
|
849
|
+
const packageJson = path.join(current, "package.json");
|
|
850
|
+
if (fs.existsSync(packageJson))
|
|
851
|
+
return current;
|
|
852
|
+
const parent = path.dirname(current);
|
|
853
|
+
if (parent === current)
|
|
854
|
+
return undefined;
|
|
855
|
+
current = parent;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
catch {
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function readPackageVersion(packageRoot) {
|
|
863
|
+
if (!packageRoot)
|
|
864
|
+
return undefined;
|
|
865
|
+
try {
|
|
866
|
+
const raw = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8"));
|
|
867
|
+
return typeof raw.version === "string" ? raw.version : undefined;
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return undefined;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
export class NetworkRecordDecryptionError extends Error {
|
|
874
|
+
kind = "NetworkRecordDecryptionError";
|
|
875
|
+
ciphertextPrefix;
|
|
876
|
+
constructor(message, ciphertextPrefix, cause) {
|
|
877
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
878
|
+
this.name = "NetworkRecordDecryptionError";
|
|
879
|
+
this.ciphertextPrefix = ciphertextPrefix;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
function pickSdkNetwork(options) {
|
|
883
|
+
if (options?.network)
|
|
884
|
+
return options.network;
|
|
885
|
+
// Reuse any already-loaded SDK module to avoid double-loading.
|
|
886
|
+
for (const network of sdkModuleCache.keys()) {
|
|
887
|
+
return normalizeSdkNetwork(network);
|
|
888
|
+
}
|
|
889
|
+
return "testnet";
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Decrypt an Aleo record ciphertext using a view key.
|
|
893
|
+
* Returns the plaintext record literal (a string suitable for downstream
|
|
894
|
+
* `deserialize<Name>` parsing). View key must already be a string view key
|
|
895
|
+
* (`AViewKey1...`). To accept a private key, call `deriveViewKey` first.
|
|
896
|
+
*/
|
|
897
|
+
export async function decryptRecordCiphertext(ciphertext, viewKey, options) {
|
|
898
|
+
const prefix = typeof ciphertext === "string" ? ciphertext.slice(0, 16) : "(non-string)";
|
|
899
|
+
if (typeof ciphertext !== "string" || ciphertext.length === 0) {
|
|
900
|
+
throw new NetworkRecordDecryptionError("Record ciphertext must be a non-empty string.", prefix);
|
|
901
|
+
}
|
|
902
|
+
if (!ciphertext.startsWith("record1")) {
|
|
903
|
+
throw new NetworkRecordDecryptionError(`Record ciphertext must start with "record1". Received prefix ${JSON.stringify(prefix)}.`, prefix);
|
|
904
|
+
}
|
|
905
|
+
if (typeof viewKey !== "string" || !viewKey.startsWith("AViewKey1")) {
|
|
906
|
+
throw new NetworkRecordDecryptionError('View key must be a string starting with "AViewKey1". To pass a private key, call deriveViewKey() first.', prefix);
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const sdk = await loadSdkModule(pickSdkNetwork(options));
|
|
910
|
+
const vk = sdk.ViewKey.from_string(viewKey);
|
|
911
|
+
const ct = sdk.RecordCiphertext.fromString(ciphertext);
|
|
912
|
+
const plaintext = ct.decrypt(vk);
|
|
913
|
+
// SDK returns RecordPlaintext; toString() yields the Leo literal.
|
|
914
|
+
return plaintext.toString();
|
|
915
|
+
}
|
|
916
|
+
catch (cause) {
|
|
917
|
+
if (cause instanceof NetworkRecordDecryptionError)
|
|
918
|
+
throw cause;
|
|
919
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
920
|
+
throw new NetworkRecordDecryptionError(`Failed to decrypt record ciphertext (prefix ${JSON.stringify(prefix)}): ${message}`, prefix, cause);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
// ---------------------------------------------------------------------------
|
|
924
|
+
// Value ciphertext decryption (private plaintext outputs / inputs)
|
|
925
|
+
// ---------------------------------------------------------------------------
|
|
926
|
+
export class NetworkValueDecryptionError extends Error {
|
|
927
|
+
kind = "NetworkValueDecryptionError";
|
|
928
|
+
ciphertextPrefix;
|
|
929
|
+
constructor(message, ciphertextPrefix, cause) {
|
|
930
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
931
|
+
this.name = "NetworkValueDecryptionError";
|
|
932
|
+
this.ciphertextPrefix = ciphertextPrefix;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Decrypt an Aleo value ciphertext (`ciphertext1...`) — the on-wire form for a
|
|
937
|
+
* private plaintext input or output of a transition. Distinct from record
|
|
938
|
+
* ciphertexts, which use a different (record-owner-derived) encryption scheme.
|
|
939
|
+
*
|
|
940
|
+
* Returns the decrypted Leo literal as a string (e.g. `"10000u64"`, `"true"`,
|
|
941
|
+
* `"aleo1..."`), suitable for downstream primitive parsers.
|
|
942
|
+
*
|
|
943
|
+
* `globalIndex` is the position of the input or output in the transition's
|
|
944
|
+
* combined input+output list — Aleo's domain separation places inputs first.
|
|
945
|
+
* For a transition with `N` inputs, output ABI index `i` corresponds to global
|
|
946
|
+
* index `N + i`.
|
|
947
|
+
*/
|
|
948
|
+
export async function decryptValueCiphertext(ciphertext, viewKey, tpk, programId, transitionName, globalIndex, options) {
|
|
949
|
+
const prefix = typeof ciphertext === "string" ? ciphertext.slice(0, 16) : "(non-string)";
|
|
950
|
+
if (typeof ciphertext !== "string" || ciphertext.length === 0) {
|
|
951
|
+
throw new NetworkValueDecryptionError("Value ciphertext must be a non-empty string.", prefix);
|
|
952
|
+
}
|
|
953
|
+
if (!ciphertext.startsWith("ciphertext1")) {
|
|
954
|
+
throw new NetworkValueDecryptionError(`Value ciphertext must start with "ciphertext1". Received prefix ${JSON.stringify(prefix)}.`, prefix);
|
|
955
|
+
}
|
|
956
|
+
if (typeof viewKey !== "string" || !viewKey.startsWith("AViewKey1")) {
|
|
957
|
+
throw new NetworkValueDecryptionError('View key must be a string starting with "AViewKey1". To pass a private key, call deriveViewKey() first.', prefix);
|
|
958
|
+
}
|
|
959
|
+
if (typeof tpk !== "string" || tpk.length === 0) {
|
|
960
|
+
throw new NetworkValueDecryptionError("Transition public key (tpk) must be a non-empty string.", prefix);
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
const sdk = await loadSdkModule(pickSdkNetwork(options));
|
|
964
|
+
// WASM objects are consumed by decryptWithTransitionInfo — caller-side
|
|
965
|
+
// reuse triggers "null pointer passed to rust". Construct fresh per call.
|
|
966
|
+
const vk = sdk.ViewKey.from_string(viewKey);
|
|
967
|
+
const tpkGroup = sdk.Group.fromString(tpk);
|
|
968
|
+
const ct = sdk.Ciphertext.fromString(ciphertext);
|
|
969
|
+
const plaintext = ct.decryptWithTransitionInfo(vk, tpkGroup, programId, transitionName, globalIndex);
|
|
970
|
+
return plaintext.toString();
|
|
971
|
+
}
|
|
972
|
+
catch (cause) {
|
|
973
|
+
if (cause instanceof NetworkValueDecryptionError)
|
|
974
|
+
throw cause;
|
|
975
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
976
|
+
throw new NetworkValueDecryptionError(`Failed to decrypt value ciphertext (prefix ${JSON.stringify(prefix)}): ${message}`, prefix, cause);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Derive a view key string (`AViewKey1...`) from a private key string
|
|
981
|
+
* (`APrivateKey1...`). Used by callers that pass private keys to decrypt.
|
|
982
|
+
*/
|
|
983
|
+
export async function deriveViewKey(privateKey, options) {
|
|
984
|
+
if (typeof privateKey !== "string" || !privateKey.startsWith("APrivateKey1")) {
|
|
985
|
+
throw new NetworkRecordDecryptionError('Private key must be a string starting with "APrivateKey1".', typeof privateKey === "string" ? privateKey.slice(0, 16) : "(non-string)");
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
const sdk = await loadSdkModule(pickSdkNetwork(options));
|
|
989
|
+
const pk = sdk.PrivateKey.from_string(privateKey);
|
|
990
|
+
return pk.to_view_key().to_string();
|
|
991
|
+
}
|
|
992
|
+
catch (cause) {
|
|
993
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
994
|
+
throw new NetworkRecordDecryptionError(`Failed to derive view key: ${message}`, privateKey.slice(0, 16), cause);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
//# sourceMappingURL=sdk-adapter.js.map
|