zk-credits 0.2.1 → 0.2.2
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 +32 -18
- package/dist/cli.js +91 -22
- package/dist/setup-config.d.ts +36 -0
- package/dist/setup-config.js +320 -0
- package/dist/zk-credits.js +495 -142
- package/package.json +1 -1
- package/dist/setup-preflight.d.ts +0 -2
- package/dist/setup-preflight.js +0 -12
package/README.md
CHANGED
|
@@ -17,15 +17,17 @@ not mix versions between the sidecar, the adapter package, and the gateway:
|
|
|
17
17
|
|
|
18
18
|
| Package | Version |
|
|
19
19
|
| --- | --- |
|
|
20
|
-
| `zk-credits` (this sidecar) | `0.2.
|
|
20
|
+
| `zk-credits` (this sidecar) | `0.2.1` |
|
|
21
21
|
| `@zk-credits/x402-zk-prepaid` (adapter) | `0.1.0` |
|
|
22
22
|
| `@zk-credits/shared` | `0.1.0` |
|
|
23
23
|
|
|
24
|
-
`zk-credits@0.2.
|
|
25
|
-
uses the non-streaming `POST /v1/chat/completions` path below
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
`zk-credits@0.2.1` is the currently published Base Sepolia pilot version. It
|
|
25
|
+
uses the non-streaming `POST /v1/chat/completions` path below and includes the
|
|
26
|
+
loopback Responses bridge for Codex. The local `0.2.2` candidate adds local
|
|
27
|
+
discovery of the pinned proving bundle and witness, validates them against the
|
|
28
|
+
activated credential, and saves their paths for later runs. It is not yet
|
|
29
|
+
published or part of the pinned pilot set. Neither version carries a Stellar
|
|
30
|
+
or evaluation path. The proving artifacts
|
|
29
31
|
(`private-credit-spend-bn254-dev-sepolia-v1`) are delivered directly through
|
|
30
32
|
the invite channel, never from a public host.
|
|
31
33
|
|
|
@@ -60,31 +62,43 @@ still receives the class. A rejected request consumes no credit.
|
|
|
60
62
|
1. Redeem an invite and download the encrypted credential. Credits are
|
|
61
63
|
founder-provisioned test credits; there is no payment step.
|
|
62
64
|
2. Install `zk-credits` and ask your pilot contact for the pinned proving
|
|
63
|
-
bundle and public Base tree file.
|
|
65
|
+
bundle and public Base tree file. Put them in
|
|
66
|
+
`~/.zk-credits/artifacts/base-sepolia` and `~/.zk-credits/witnesses`, or
|
|
67
|
+
place them in `~/Downloads`. The package ships
|
|
64
68
|
`circuits/manifest.json`, which fixes the SHA-256 of the frozen
|
|
65
69
|
`private_credit_spend.wasm`, `private_credit_spend.zkey`, and
|
|
66
70
|
`verification_key_private_credit.json`. The sidecar never fetches proving
|
|
67
|
-
material at runtime. Set the
|
|
71
|
+
material at runtime. Set the credential and gateway:
|
|
68
72
|
|
|
69
73
|
```bash
|
|
70
74
|
export ZK_CREDITS_CREDENTIAL_PATH="$HOME/Downloads/zk-credits-credential.json"
|
|
71
|
-
export ZK_CREDITS_ARTIFACT_DIR="/path/to/pinned-bundle"
|
|
72
|
-
export ZK_CREDITS_WITNESS_PATH="/path/to/base-tree.json"
|
|
73
75
|
export ZK_CREDITS_GATEWAY_URL="https://zk-credits-gateway.onrender.com"
|
|
74
76
|
```
|
|
75
77
|
|
|
78
|
+
`zk-credits setup codex` searches those local folders, validates the bundle
|
|
79
|
+
hashes and witness for the activated credential, then saves the selected
|
|
80
|
+
local paths in `~/.zk-credits/config.json`. Set
|
|
81
|
+
`ZK_CREDITS_ARTIFACT_DIR` or `ZK_CREDITS_WITNESS_PATH` to select explicit
|
|
82
|
+
paths. Alternatively, set `BASE_RPC_URL` and
|
|
83
|
+
`BASE_PRIVATE_CREDIT_BOND_ADDRESS` (and optionally
|
|
84
|
+
`BASE_DEPLOYMENT_BLOCK`) to derive a witness from public Base events. Event
|
|
85
|
+
sync requires RPC access and is validated locally before setup completes.
|
|
86
|
+
The config file is owner-readable only and never stores the credential
|
|
87
|
+
password. If an RPC URL contains a provider key, it remains in that local
|
|
88
|
+
config file.
|
|
89
|
+
|
|
76
90
|
A missing, relocated, symlinked-out, or altered artifact fails closed
|
|
77
91
|
before the first prove. A hash mismatch is a proof failure: no payment
|
|
78
92
|
leaves, and the local slot stays reusable.
|
|
79
93
|
|
|
80
|
-
`ZK_CREDITS_WITNESS_PATH` points to a local witness artifact —
|
|
81
|
-
prepared witness (`root`, `pathElements`, `pathIndices`) or a
|
|
82
|
-
(`leaves: [{ index, leaf, expiry? }]`, optional `root`) from
|
|
83
|
-
sidecar derives the depth-20 path — or configure `BASE_RPC_URL`,
|
|
84
|
-
`BASE_PRIVATE_CREDIT_BOND_ADDRESS`, and optionally
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
94
|
+
An explicit `ZK_CREDITS_WITNESS_PATH` points to a local witness artifact —
|
|
95
|
+
either a prepared witness (`root`, `pathElements`, `pathIndices`) or a
|
|
96
|
+
public tree (`leaves: [{ index, leaf, expiry? }]`, optional `root`) from
|
|
97
|
+
which the sidecar derives the depth-20 path — or configure `BASE_RPC_URL`,
|
|
98
|
+
`BASE_PRIVATE_CREDIT_BOND_ADDRESS`, and optionally `BASE_DEPLOYMENT_BLOCK`
|
|
99
|
+
so the sidecar synchronizes public `BundleFunded` events and builds a local
|
|
100
|
+
Merkle witness. The gateway never serves a path for a named leaf or
|
|
101
|
+
commitment.
|
|
88
102
|
|
|
89
103
|
3. Start setup. It verifies the credential, pinned artifact hashes, and
|
|
90
104
|
witness locally, then prompts for the backup password without echo:
|
package/dist/cli.js
CHANGED
|
@@ -17,17 +17,26 @@ import { runCliCommand } from './cli-runtime.js';
|
|
|
17
17
|
import { createNodeSidecarLifecycle } from './node-sidecar-lifecycle.js';
|
|
18
18
|
import { activateSidecarServer } from './server-startup.js';
|
|
19
19
|
import { createLoopbackToken, sidecarStatePaths } from './sidecar-config.js';
|
|
20
|
+
import { applySetupConfig, readSetupConfig, resolveSetupInputs, writeSetupConfig, } from './setup-config.js';
|
|
20
21
|
import { ensureSidecarReady } from './sidecar-lifecycle.js';
|
|
21
22
|
import { readLoopbackToken, writeLoopbackToken } from './sidecar-state.js';
|
|
22
23
|
import { createSidecarServer } from './sidecar.js';
|
|
23
24
|
import { runBaseRegisteredTrialExchange } from './base-registered-trial.js';
|
|
24
|
-
import {
|
|
25
|
+
import { loadCircuitManifest } from './artifact-bundle.js';
|
|
25
26
|
import { BASE_SEPOLIA_NETWORK } from '@zk-credits/x402-zk-prepaid';
|
|
26
27
|
const DEFAULT_GATEWAY_URL = 'https://zk-credits-gateway.onrender.com';
|
|
27
28
|
const DEFAULT_PORT = 3210;
|
|
28
29
|
function stateDirectory() {
|
|
29
30
|
return process.env.ZK_CREDITS_HOME || join(homedir(), '.zk-credits');
|
|
30
31
|
}
|
|
32
|
+
function setupConfigPath(directory = stateDirectory()) {
|
|
33
|
+
return join(directory, 'config.json');
|
|
34
|
+
}
|
|
35
|
+
async function applySavedSetupConfig(environment, directory = stateDirectory()) {
|
|
36
|
+
const config = await readSetupConfig(setupConfigPath(directory));
|
|
37
|
+
if (config)
|
|
38
|
+
applySetupConfig(environment, config);
|
|
39
|
+
}
|
|
31
40
|
function loopbackBaseUrl() {
|
|
32
41
|
const port = Number(process.env.ZK_CREDITS_SIDECAR_PORT || DEFAULT_PORT);
|
|
33
42
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
@@ -45,6 +54,45 @@ function readPort(args) {
|
|
|
45
54
|
}
|
|
46
55
|
return parsed;
|
|
47
56
|
}
|
|
57
|
+
async function chooseSetupCandidate(kind, candidates) {
|
|
58
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
59
|
+
const variable = kind === 'proving bundle' ? 'ZK_CREDITS_ARTIFACT_DIR' : 'ZK_CREDITS_WITNESS_PATH';
|
|
60
|
+
throw new Error('Found more than one valid local ' + kind + '. Rerun in an interactive terminal or set ' + variable + ' explicitly.');
|
|
61
|
+
}
|
|
62
|
+
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
63
|
+
try {
|
|
64
|
+
console.log('Found multiple valid ' + kind + 's:');
|
|
65
|
+
candidates.forEach((candidate, index) => console.log(' ' + (index + 1) + ') ' + candidate));
|
|
66
|
+
const answer = await input.question('Select a number: ');
|
|
67
|
+
const index = Number(answer);
|
|
68
|
+
if (!Number.isInteger(index) || index < 1 || index > candidates.length) {
|
|
69
|
+
throw new Error('Choose one of the listed local candidates');
|
|
70
|
+
}
|
|
71
|
+
return candidates[index - 1];
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
input.close();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function witnessProviderForSource(source, cachePath) {
|
|
78
|
+
if (source.kind === 'file') {
|
|
79
|
+
let value;
|
|
80
|
+
try {
|
|
81
|
+
value = JSON.parse(await readFile(source.path, 'utf8'));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new Error('The Base witness file could not be read as JSON');
|
|
85
|
+
}
|
|
86
|
+
return createFileWitnessProvider(value);
|
|
87
|
+
}
|
|
88
|
+
return createBaseEventWitnessProvider({
|
|
89
|
+
rpcUrl: source.rpcUrl,
|
|
90
|
+
contractAddress: source.contractAddress,
|
|
91
|
+
deploymentBlock: source.deploymentBlock === undefined ? undefined : BigInt(source.deploymentBlock),
|
|
92
|
+
confirmations: source.confirmations === undefined ? undefined : BigInt(source.confirmations),
|
|
93
|
+
cachePath,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
48
96
|
/** Reads a password written to the private stdin pipe by the foreground CLI. */
|
|
49
97
|
async function readCredentialPasswordFromStdin() {
|
|
50
98
|
const chunks = [];
|
|
@@ -125,9 +173,11 @@ non-streaming POST /v1/chat/completions; Codex Responses requests pass through
|
|
|
125
173
|
the sidecar's bounded local compatibility bridge.
|
|
126
174
|
"zk-credits trial-registered-adapter" runs one confirmed, billable internal
|
|
127
175
|
trial request through the directly registered official x402 client adapter.
|
|
128
|
-
Set ZK_CREDITS_CREDENTIAL_PATH
|
|
129
|
-
|
|
130
|
-
|
|
176
|
+
Set ZK_CREDITS_CREDENTIAL_PATH before setup. The wizard searches Downloads and
|
|
177
|
+
~/.zk-credits for the pinned bundle and local Base witness, validates both,
|
|
178
|
+
then saves their paths locally. Alternatively, set ZK_CREDITS_ARTIFACT_DIR and
|
|
179
|
+
ZK_CREDITS_WITNESS_PATH explicitly. Setup prompts for the credential password
|
|
180
|
+
without echo; the encrypted export is decrypted only in local memory.`);
|
|
131
181
|
}
|
|
132
182
|
/** Loads the same credential, witness, proof, and durable ledger used by serve. */
|
|
133
183
|
async function loadBaseClientRuntime(passwordInput) {
|
|
@@ -472,6 +522,10 @@ async function main() {
|
|
|
472
522
|
printHelp();
|
|
473
523
|
return;
|
|
474
524
|
}
|
|
525
|
+
const sidecarHome = stateDirectory();
|
|
526
|
+
if (args[0] !== 'setup' && args[0] !== 'status' && args[0] !== 'env') {
|
|
527
|
+
await applySavedSetupConfig(process.env, sidecarHome);
|
|
528
|
+
}
|
|
475
529
|
if (args[0] === 'serve') {
|
|
476
530
|
const passwordFromStdin = args.includes('--credential-password-stdin')
|
|
477
531
|
? await readCredentialPasswordFromStdin()
|
|
@@ -483,19 +537,26 @@ async function main() {
|
|
|
483
537
|
await trialRegisteredAdapter();
|
|
484
538
|
return;
|
|
485
539
|
}
|
|
486
|
-
const sidecarHome = stateDirectory();
|
|
487
540
|
const statePaths = sidecarStatePaths(sidecarHome);
|
|
488
541
|
const codexHome = resolveCodexHome(process.env, homedir());
|
|
489
542
|
const cliEntryPath = process.argv[1];
|
|
490
543
|
if (!cliEntryPath)
|
|
491
544
|
throw new Error('Unable to resolve the zk-credits executable path');
|
|
545
|
+
let setupCredentialPassword;
|
|
492
546
|
const lifecycle = createNodeSidecarLifecycle({
|
|
493
547
|
loopbackBaseUrl: loopbackBaseUrl(),
|
|
494
548
|
stateDirectory: sidecarHome,
|
|
495
549
|
tokenPath: statePaths.tokenPath,
|
|
496
550
|
logPath: statePaths.logPath,
|
|
497
551
|
cliEntryPath,
|
|
498
|
-
readCredentialPassword: () =>
|
|
552
|
+
readCredentialPassword: async () => {
|
|
553
|
+
if (setupCredentialPassword !== undefined) {
|
|
554
|
+
const password = setupCredentialPassword;
|
|
555
|
+
setupCredentialPassword = undefined;
|
|
556
|
+
return password;
|
|
557
|
+
}
|
|
558
|
+
return readHiddenValue('Credential backup password: ');
|
|
559
|
+
},
|
|
499
560
|
});
|
|
500
561
|
const exitCode = await runCliCommand(args, {
|
|
501
562
|
loopbackBaseUrl: loopbackBaseUrl(),
|
|
@@ -514,22 +575,25 @@ async function main() {
|
|
|
514
575
|
}
|
|
515
576
|
},
|
|
516
577
|
validateSetupPrerequisites: async () => {
|
|
517
|
-
const
|
|
518
|
-
if (
|
|
519
|
-
throw new Error(
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
578
|
+
const credentialPath = process.env.ZK_CREDITS_CREDENTIAL_PATH;
|
|
579
|
+
if (!credentialPath?.trim())
|
|
580
|
+
throw new Error('Set ZK_CREDITS_CREDENTIAL_PATH to the encrypted browser export');
|
|
581
|
+
const password = await readHiddenValue('Credential backup password: ');
|
|
582
|
+
const credential = await readEncryptedCredential(credentialPath, password);
|
|
583
|
+
const manifest = await loadCircuitManifest();
|
|
584
|
+
const config = await resolveSetupInputs({
|
|
585
|
+
environment: process.env,
|
|
586
|
+
homeDirectory: homedir(),
|
|
587
|
+
stateDirectory: sidecarHome,
|
|
588
|
+
configPath: setupConfigPath(sidecarHome),
|
|
589
|
+
manifest,
|
|
590
|
+
validateWitness: async (source) => {
|
|
591
|
+
const provider = await witnessProviderForSource(source, join(stateDirectory(), 'base-event-sync.json'));
|
|
592
|
+
validateBaseCreditWitness(await provider.witnessForCredential(credential));
|
|
593
|
+
},
|
|
594
|
+
chooseCandidate: chooseSetupCandidate,
|
|
595
|
+
});
|
|
596
|
+
const artifactDirectory = config.artifactDirectory;
|
|
533
597
|
try {
|
|
534
598
|
await createPinnedBaseProofGenerator({
|
|
535
599
|
artifactDirectory,
|
|
@@ -541,6 +605,11 @@ async function main() {
|
|
|
541
605
|
throw new Error(`Pinned proving bundle check failed: ${error.message}`);
|
|
542
606
|
throw new Error('Pinned proving bundle check failed. Reinstall the bundle from your pilot invite.');
|
|
543
607
|
}
|
|
608
|
+
await writeSetupConfig(setupConfigPath(sidecarHome), config);
|
|
609
|
+
applySetupConfig(process.env, config);
|
|
610
|
+
setupCredentialPassword = password;
|
|
611
|
+
console.log('Pinned proving bundle and witness validated for this activated credential.');
|
|
612
|
+
console.log('Local setup paths saved to ' + setupConfigPath(sidecarHome));
|
|
544
613
|
},
|
|
545
614
|
configureCodex: async (model) => {
|
|
546
615
|
await writeCodexProfile({ codexHome, loopbackBaseUrl: loopbackBaseUrl(), model });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type CircuitManifest } from './artifact-bundle.js';
|
|
2
|
+
export type SetupWitnessSource = {
|
|
3
|
+
kind: 'file';
|
|
4
|
+
path: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'base-events';
|
|
7
|
+
rpcUrl: string;
|
|
8
|
+
contractAddress: string;
|
|
9
|
+
deploymentBlock?: string;
|
|
10
|
+
confirmations?: string;
|
|
11
|
+
};
|
|
12
|
+
export interface SetupConfig {
|
|
13
|
+
version: 1;
|
|
14
|
+
artifactDirectory: string;
|
|
15
|
+
witness: SetupWitnessSource;
|
|
16
|
+
}
|
|
17
|
+
export interface ResolveSetupInputsOptions {
|
|
18
|
+
environment: NodeJS.ProcessEnv;
|
|
19
|
+
homeDirectory: string;
|
|
20
|
+
stateDirectory: string;
|
|
21
|
+
configPath: string;
|
|
22
|
+
manifest: CircuitManifest;
|
|
23
|
+
/** Must prove this source resolves a valid witness for the activated credential. */
|
|
24
|
+
validateWitness(source: SetupWitnessSource): Promise<void>;
|
|
25
|
+
/** Called only when more than one valid local candidate exists. */
|
|
26
|
+
chooseCandidate?(kind: 'proving bundle' | 'witness file', candidates: readonly string[]): Promise<string>;
|
|
27
|
+
workingDirectory?: string;
|
|
28
|
+
}
|
|
29
|
+
/** Finds only known local locations and validates both sources before setup continues. */
|
|
30
|
+
export declare function resolveSetupInputs(options: ResolveSetupInputsOptions): Promise<SetupConfig>;
|
|
31
|
+
/** Reads the operator-local setup config. Missing config is normal on first run. */
|
|
32
|
+
export declare function readSetupConfig(path: string): Promise<SetupConfig | undefined>;
|
|
33
|
+
/** Atomically writes paths and witness configuration with owner-only permissions. */
|
|
34
|
+
export declare function writeSetupConfig(path: string, config: SetupConfig): Promise<void>;
|
|
35
|
+
/** Applies saved paths to a process environment while preserving explicit overrides. */
|
|
36
|
+
export declare function applySetupConfig(environment: NodeJS.ProcessEnv, config: SetupConfig): void;
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { resolvePinnedArtifactBundle } from './artifact-bundle.js';
|
|
5
|
+
const WITNESS_FILENAMES = new Set([
|
|
6
|
+
'base-tree.json',
|
|
7
|
+
'base-sepolia-tree.json',
|
|
8
|
+
'base-tree-artifact.json',
|
|
9
|
+
'public-tree.json',
|
|
10
|
+
'base-witness.json',
|
|
11
|
+
'zk-credits-witness.json',
|
|
12
|
+
'witness.json',
|
|
13
|
+
'tree.json',
|
|
14
|
+
]);
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function absolutePath(value, label) {
|
|
19
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
20
|
+
throw new Error('Setup configuration ' + label + ' is required');
|
|
21
|
+
}
|
|
22
|
+
return resolve(value);
|
|
23
|
+
}
|
|
24
|
+
function optionalUnsignedString(value, label) {
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof value !== 'string' || !/^\d+$/u.test(value)) {
|
|
28
|
+
throw new Error('Local setup config ' + label + ' must be a non-negative integer');
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function parseSetupConfig(value) {
|
|
33
|
+
if (!isRecord(value) || value.version !== 1 || !isRecord(value.witness)) {
|
|
34
|
+
throw new Error('Local setup config is malformed; remove ~/.zk-credits/config.json and run setup again.');
|
|
35
|
+
}
|
|
36
|
+
const artifactDirectory = absolutePath(value.artifactDirectory, 'artifactDirectory');
|
|
37
|
+
const witnessValue = value.witness;
|
|
38
|
+
let witness;
|
|
39
|
+
if (witnessValue.kind === 'file') {
|
|
40
|
+
witness = { kind: 'file', path: absolutePath(witnessValue.path, 'witness.path') };
|
|
41
|
+
}
|
|
42
|
+
else if (witnessValue.kind === 'base-events') {
|
|
43
|
+
if (typeof witnessValue.rpcUrl !== 'string' || !/^https?:\/\//u.test(witnessValue.rpcUrl)) {
|
|
44
|
+
throw new Error('Local setup config has an invalid Base RPC URL');
|
|
45
|
+
}
|
|
46
|
+
if (typeof witnessValue.contractAddress !== 'string' || !/^0x[0-9a-fA-F]{40}$/u.test(witnessValue.contractAddress)) {
|
|
47
|
+
throw new Error('Local setup config has an invalid Base bond contract address');
|
|
48
|
+
}
|
|
49
|
+
const deploymentBlock = optionalUnsignedString(witnessValue.deploymentBlock, 'deploymentBlock');
|
|
50
|
+
const confirmations = optionalUnsignedString(witnessValue.confirmations, 'confirmations');
|
|
51
|
+
witness = {
|
|
52
|
+
kind: 'base-events',
|
|
53
|
+
rpcUrl: witnessValue.rpcUrl,
|
|
54
|
+
contractAddress: witnessValue.contractAddress,
|
|
55
|
+
...(deploymentBlock === undefined ? {} : { deploymentBlock }),
|
|
56
|
+
...(confirmations === undefined ? {} : { confirmations }),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
throw new Error('Local setup config has an unknown witness source');
|
|
61
|
+
}
|
|
62
|
+
return { version: 1, artifactDirectory, witness };
|
|
63
|
+
}
|
|
64
|
+
function optionalEnvironmentInteger(environment, name) {
|
|
65
|
+
const value = environment[name]?.trim();
|
|
66
|
+
if (!value)
|
|
67
|
+
return undefined;
|
|
68
|
+
if (!/^\d+$/u.test(value))
|
|
69
|
+
throw new Error(name + ' must be a non-negative integer');
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
async function candidateDirectories(homeDirectory, stateDirectory) {
|
|
73
|
+
const roots = [
|
|
74
|
+
join(stateDirectory, 'artifacts', 'base-sepolia'),
|
|
75
|
+
join(homeDirectory, '.zk-credits', 'artifacts', 'base-sepolia'),
|
|
76
|
+
join(homeDirectory, 'Downloads'),
|
|
77
|
+
];
|
|
78
|
+
const candidates = [...roots];
|
|
79
|
+
for (const root of roots) {
|
|
80
|
+
let entries;
|
|
81
|
+
try {
|
|
82
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (entry.isDirectory())
|
|
89
|
+
candidates.push(join(root, entry.name));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...new Set(candidates.map((path) => resolve(path)))];
|
|
93
|
+
}
|
|
94
|
+
async function chooseOne(kind, candidates, chooseCandidate) {
|
|
95
|
+
if (candidates.length === 1)
|
|
96
|
+
return candidates[0];
|
|
97
|
+
if (!chooseCandidate) {
|
|
98
|
+
const variable = kind === 'proving bundle' ? 'ZK_CREDITS_ARTIFACT_DIR' : 'ZK_CREDITS_WITNESS_PATH';
|
|
99
|
+
throw new Error('Found more than one valid ' + kind + '; set ' + variable + ' to choose one.');
|
|
100
|
+
}
|
|
101
|
+
const selected = await chooseCandidate(kind, candidates);
|
|
102
|
+
if (!candidates.includes(resolve(selected))) {
|
|
103
|
+
throw new Error('The selected ' + kind + ' is not one of the validated local candidates');
|
|
104
|
+
}
|
|
105
|
+
return resolve(selected);
|
|
106
|
+
}
|
|
107
|
+
async function resolveArtifactDirectory(options, saved) {
|
|
108
|
+
const explicit = options.environment.ZK_CREDITS_ARTIFACT_DIR?.trim();
|
|
109
|
+
const candidates = explicit
|
|
110
|
+
? [resolve(options.workingDirectory ?? process.cwd(), explicit)]
|
|
111
|
+
: [
|
|
112
|
+
...(saved ? [saved.artifactDirectory] : []),
|
|
113
|
+
...await candidateDirectories(options.homeDirectory, options.stateDirectory),
|
|
114
|
+
];
|
|
115
|
+
const valid = new Map();
|
|
116
|
+
let explicitError;
|
|
117
|
+
for (const candidate of candidates) {
|
|
118
|
+
try {
|
|
119
|
+
const bundle = await resolvePinnedArtifactBundle({
|
|
120
|
+
artifactDirectory: candidate,
|
|
121
|
+
manifest: options.manifest,
|
|
122
|
+
});
|
|
123
|
+
let identity = resolve(bundle.directory);
|
|
124
|
+
try {
|
|
125
|
+
identity = await realpath(bundle.directory);
|
|
126
|
+
}
|
|
127
|
+
catch { /* resolve already validated the directory */ }
|
|
128
|
+
valid.set(identity, bundle.directory);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (explicit)
|
|
132
|
+
explicitError = error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (explicit && valid.size === 0) {
|
|
136
|
+
const reason = explicitError instanceof Error ? ' ' + explicitError.message : '';
|
|
137
|
+
throw new Error('The configured proving bundle at ' + explicit + ' failed its pinned checks.' + reason);
|
|
138
|
+
}
|
|
139
|
+
if (valid.size === 0) {
|
|
140
|
+
throw new Error('Could not find a valid pinned proving bundle. Extract the bundle from your pilot contact into ~/.zk-credits/artifacts/base-sepolia or ~/Downloads, or set ZK_CREDITS_ARTIFACT_DIR.');
|
|
141
|
+
}
|
|
142
|
+
const selected = await chooseOne('proving bundle', [...valid.values()], options.chooseCandidate);
|
|
143
|
+
return resolve(selected);
|
|
144
|
+
}
|
|
145
|
+
function eventSourceFromEnvironment(environment) {
|
|
146
|
+
const rpcUrl = environment.BASE_RPC_URL?.trim();
|
|
147
|
+
const contractAddress = environment.BASE_PRIVATE_CREDIT_BOND_ADDRESS?.trim();
|
|
148
|
+
if (!rpcUrl || !contractAddress)
|
|
149
|
+
return undefined;
|
|
150
|
+
const deploymentBlock = optionalEnvironmentInteger(environment, 'BASE_DEPLOYMENT_BLOCK');
|
|
151
|
+
const confirmations = optionalEnvironmentInteger(environment, 'BASE_CONFIRMATIONS');
|
|
152
|
+
return {
|
|
153
|
+
kind: 'base-events',
|
|
154
|
+
rpcUrl,
|
|
155
|
+
contractAddress,
|
|
156
|
+
...(deploymentBlock === undefined ? {} : { deploymentBlock }),
|
|
157
|
+
...(confirmations === undefined ? {} : { confirmations }),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function witnessCandidates(homeDirectory, stateDirectory) {
|
|
161
|
+
const roots = [
|
|
162
|
+
join(stateDirectory, 'witnesses'),
|
|
163
|
+
join(stateDirectory, 'witness'),
|
|
164
|
+
join(homeDirectory, '.zk-credits', 'witnesses'),
|
|
165
|
+
join(homeDirectory, 'Downloads'),
|
|
166
|
+
];
|
|
167
|
+
return [...new Set(roots.flatMap((root) => [...WITNESS_FILENAMES].map((name) => resolve(root, name))))];
|
|
168
|
+
}
|
|
169
|
+
async function resolveWitnessSource(options, saved) {
|
|
170
|
+
const environment = options.environment;
|
|
171
|
+
const explicitPath = environment.ZK_CREDITS_WITNESS_PATH?.trim();
|
|
172
|
+
if (explicitPath) {
|
|
173
|
+
const source = {
|
|
174
|
+
kind: 'file',
|
|
175
|
+
path: resolve(options.workingDirectory ?? process.cwd(), explicitPath),
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
await options.validateWitness(source);
|
|
179
|
+
return source;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const reason = error instanceof Error ? ' ' + error.message : '';
|
|
183
|
+
throw new Error('The configured Base witness at ' + explicitPath + ' failed validation.' + reason);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const environmentSource = eventSourceFromEnvironment(environment);
|
|
187
|
+
if (environmentSource) {
|
|
188
|
+
try {
|
|
189
|
+
await options.validateWitness(environmentSource);
|
|
190
|
+
return environmentSource;
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const reason = error instanceof Error ? ' ' + error.message : '';
|
|
194
|
+
throw new Error('Base event witness sync failed.' + reason);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (saved?.witness.kind === 'base-events') {
|
|
198
|
+
try {
|
|
199
|
+
await options.validateWitness(saved.witness);
|
|
200
|
+
return saved.witness;
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
const reason = error instanceof Error ? ' ' + error.message : '';
|
|
204
|
+
throw new Error('Saved Base event witness source failed validation.' + reason);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (saved?.witness.kind === 'file') {
|
|
208
|
+
try {
|
|
209
|
+
await options.validateWitness(saved.witness);
|
|
210
|
+
return saved.witness;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// A saved local witness can become stale for a different activated credential.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const valid = [];
|
|
217
|
+
for (const path of witnessCandidates(options.homeDirectory, options.stateDirectory)) {
|
|
218
|
+
try {
|
|
219
|
+
const info = await lstat(path);
|
|
220
|
+
if (!info.isFile())
|
|
221
|
+
continue;
|
|
222
|
+
const source = { kind: 'file', path };
|
|
223
|
+
await options.validateWitness(source);
|
|
224
|
+
valid.push(path);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// Ignore absent, unrelated, and non-resolving local files while searching.
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (valid.length > 0) {
|
|
231
|
+
return { kind: 'file', path: await chooseOne('witness file', valid, options.chooseCandidate) };
|
|
232
|
+
}
|
|
233
|
+
throw new Error('Could not find a public Base witness for this credential. Put base-tree.json or witness.json in ~/.zk-credits/witnesses or ~/Downloads, set ZK_CREDITS_WITNESS_PATH, or configure BASE_RPC_URL and BASE_PRIVATE_CREDIT_BOND_ADDRESS.');
|
|
234
|
+
}
|
|
235
|
+
/** Finds only known local locations and validates both sources before setup continues. */
|
|
236
|
+
export async function resolveSetupInputs(options) {
|
|
237
|
+
const saved = await readSetupConfig(options.configPath);
|
|
238
|
+
const artifactDirectory = await resolveArtifactDirectory(options, saved);
|
|
239
|
+
const witness = await resolveWitnessSource(options, saved);
|
|
240
|
+
return { version: 1, artifactDirectory, witness };
|
|
241
|
+
}
|
|
242
|
+
/** Reads the operator-local setup config. Missing config is normal on first run. */
|
|
243
|
+
export async function readSetupConfig(path) {
|
|
244
|
+
let raw;
|
|
245
|
+
try {
|
|
246
|
+
const info = await lstat(path);
|
|
247
|
+
if (!info.isFile())
|
|
248
|
+
throw new Error('Local setup config must be a regular file');
|
|
249
|
+
raw = await readFile(path, 'utf8');
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT')
|
|
253
|
+
return undefined;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
return parseSetupConfig(JSON.parse(raw));
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (error instanceof SyntaxError) {
|
|
261
|
+
throw new Error('Local setup config is not valid JSON; remove ~/.zk-credits/config.json and run setup again.');
|
|
262
|
+
}
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Atomically writes paths and witness configuration with owner-only permissions. */
|
|
267
|
+
export async function writeSetupConfig(path, config) {
|
|
268
|
+
const normalized = parseSetupConfig(config);
|
|
269
|
+
const directory = dirname(resolve(path));
|
|
270
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
271
|
+
const destination = resolve(path);
|
|
272
|
+
try {
|
|
273
|
+
const existing = await lstat(destination);
|
|
274
|
+
if (existing.isSymbolicLink())
|
|
275
|
+
throw new Error('Local setup config cannot be a symbolic link');
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (!(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT'))
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
const temporary = join(directory, '.config-' + randomUUID() + '.tmp');
|
|
282
|
+
try {
|
|
283
|
+
const file = await open(temporary, 'wx', 0o600);
|
|
284
|
+
try {
|
|
285
|
+
await file.writeFile(JSON.stringify(normalized, null, 2) + '\n', 'utf8');
|
|
286
|
+
await file.sync();
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
await file.close();
|
|
290
|
+
}
|
|
291
|
+
await rename(temporary, destination);
|
|
292
|
+
await chmod(destination, 0o600);
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
await rm(temporary, { force: true });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Applies saved paths to a process environment while preserving explicit overrides. */
|
|
299
|
+
export function applySetupConfig(environment, config) {
|
|
300
|
+
if (!environment.ZK_CREDITS_ARTIFACT_DIR?.trim()) {
|
|
301
|
+
environment.ZK_CREDITS_ARTIFACT_DIR = config.artifactDirectory;
|
|
302
|
+
}
|
|
303
|
+
if (config.witness.kind === 'file') {
|
|
304
|
+
if (!environment.ZK_CREDITS_WITNESS_PATH?.trim()) {
|
|
305
|
+
environment.ZK_CREDITS_WITNESS_PATH = config.witness.path;
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (!environment.BASE_RPC_URL?.trim())
|
|
310
|
+
environment.BASE_RPC_URL = config.witness.rpcUrl;
|
|
311
|
+
if (!environment.BASE_PRIVATE_CREDIT_BOND_ADDRESS?.trim()) {
|
|
312
|
+
environment.BASE_PRIVATE_CREDIT_BOND_ADDRESS = config.witness.contractAddress;
|
|
313
|
+
}
|
|
314
|
+
if (!environment.BASE_DEPLOYMENT_BLOCK?.trim() && config.witness.deploymentBlock !== undefined) {
|
|
315
|
+
environment.BASE_DEPLOYMENT_BLOCK = config.witness.deploymentBlock;
|
|
316
|
+
}
|
|
317
|
+
if (!environment.BASE_CONFIRMATIONS?.trim() && config.witness.confirmations !== undefined) {
|
|
318
|
+
environment.BASE_CONFIRMATIONS = config.witness.confirmations;
|
|
319
|
+
}
|
|
320
|
+
}
|