@tokamak-private-dapps/private-state-cli 0.1.1 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3 - 2026-04-28
4
+
5
+ - Installed the Groth16 runtime during `private-state-cli --install` and reported Groth16 readiness from `--doctor`.
6
+ - Added live NVIDIA and Docker GPU probes to `--doctor`, with a hard failure when live Docker GPU readiness does not match the recorded Tokamak CLI metadata.
7
+ - Renamed `get-my-address` to `get-my-wallet-meta`, added `get-my-l1-address`, and added `list-local-wallets`.
8
+ - Documented the private-state CLI helper commands, common flow examples, and LLM agent guidance.
9
+
10
+ ## 0.1.2 - 2026-04-28
11
+
12
+ - Added `private-state-cli --doctor` to report CLI and install-time dependency versions through `tokamak-l2js`.
13
+ - Reported Tokamak zk-EVM runtime install mode, Docker mode, and CUDA runtime metadata.
14
+
3
15
  ## 0.1.1 - 2026-04-28
4
16
 
5
17
  - Updated channel balance proof generation to use the fixed Groth16 runtime workspace proof paths.
package/README.md CHANGED
@@ -8,7 +8,8 @@ Command-line client for the Tokamak private-state DApp.
8
8
  npm install -g @tokamak-private-dapps/private-state-cli
9
9
  ```
10
10
 
11
- Install the local Tokamak zk-EVM runtime workspace and public private-state deployment artifacts:
11
+ Install the local Tokamak zk-EVM runtime workspace, Groth16 runtime workspace, and public private-state deployment
12
+ artifacts:
12
13
 
13
14
  ```bash
14
15
  private-state-cli --install
@@ -27,9 +28,15 @@ Run the CLI with:
27
28
  private-state-cli <command> ...
28
29
  ```
29
30
 
31
+ Check the installed package and runtime state with:
32
+
33
+ ```bash
34
+ private-state-cli --doctor
35
+ ```
36
+
30
37
  ## Commands
31
38
 
32
- The normal private-state flow is:
39
+ A common private-state flow is:
33
40
 
34
41
  1. `create-channel`
35
42
  2. `deposit-bridge`
@@ -44,6 +51,24 @@ The normal private-state flow is:
44
51
 
45
52
  Use `private-state-cli --help` for the full command list and required options.
46
53
 
54
+ `private-state-cli --doctor` reports the CLI package version, dependency versions recorded by the last
55
+ `private-state-cli --install`, current dependency versions through `tokamak-l2js`, and Tokamak zk-EVM runtime
56
+ install mode, Docker mode, CUDA runtime metadata, live `nvidia-smi` and Docker GPU probe results, and Groth16
57
+ runtime health. The doctor check fails when the Tokamak Docker `useGpus` metadata does not match the live GPU probes.
58
+
59
+ Local helper commands:
60
+
61
+ ```bash
62
+ private-state-cli list-local-wallets --network sepolia --channel-name cuda
63
+ private-state-cli get-my-wallet-meta --wallet <WALLET_NAME> --password <PASSWORD> --network sepolia
64
+ private-state-cli get-my-l1-address --private-key <HEX>
65
+ ```
66
+
67
+ `list-local-wallets` reads only the local workspace and prints saved wallet names that can be reused with `--wallet`.
68
+ `get-my-wallet-meta` opens an encrypted local wallet and reports the stored L1/L2 identity metadata plus the current
69
+ on-chain channel registration match state. `get-my-l1-address` is a simple offline helper that derives the L1 address
70
+ for a private key.
71
+
47
72
  ## Workspace
48
73
 
49
74
  The CLI stores user workspaces under:
@@ -54,6 +79,57 @@ The CLI stores user workspaces under:
54
79
 
55
80
  Wallet data is encrypted with the password supplied to `join-channel` or `recover-wallet`.
56
81
 
82
+ ## LLM Agent Guidance
83
+
84
+ You may act as an interactive guide for users who do not understand this CLI or the private-state DApp. Assume the
85
+ user only knows that the DApp is a way to send funds privately. Translate the user's intent into safe, step-by-step CLI
86
+ actions.
87
+
88
+ Primary goal: help the user privately send funds by guiding them through the required private-state CLI commands,
89
+ explaining each step only as much as needed to proceed safely.
90
+
91
+ Operating rules:
92
+
93
+ - Do not ask the user to reveal raw private keys in chat. Use environment variable placeholders such as `$ADDR6`,
94
+ `$CREATOR`, or `$PRIVATE_STATE_TEST_PK`.
95
+ - Prefer testnet examples unless the user explicitly asks for mainnet.
96
+ - Before any proof-backed or bridge-facing workflow, ask the user to run `private-state-cli --doctor` and inspect
97
+ whether the runtime, Docker mode, CUDA/GPU probes, Groth16 runtime, and deployment artifacts are healthy.
98
+ - Use `private-state-cli list-local-wallets` to discover local wallet names instead of asking the user to inspect
99
+ filesystem paths manually.
100
+ - Use `private-state-cli get-my-l1-address --private-key "$KEY_ENV"` to derive the L1 address for a private-key
101
+ environment variable when wallet ownership needs to be identified.
102
+ - Use `private-state-cli get-my-wallet-meta --wallet <WALLET> --password <PASSWORD> --network <NETWORK>` to inspect
103
+ local wallet metadata and on-chain channel registration state.
104
+ - Use `private-state-cli get-my-bridge-fund` and `private-state-cli get-my-channel-fund` to check balances before
105
+ telling the user to move funds.
106
+ - Explain that wallet names are local CLI identifiers, while private transfers use notes owned by L2 addresses
107
+ registered in the channel.
108
+ - Do not present one fixed command sequence as universally correct. Some flows start from an existing channel or wallet,
109
+ while others require creating or joining a channel first.
110
+ - When the user asks for a transfer, first determine whether the sender has minted notes available. If not, guide them
111
+ through funding the bridge, joining or recovering the channel wallet, depositing into the channel, and minting notes.
112
+ - When generating commands, use placeholders for secrets and explicit values for public fields. Show one command at a
113
+ time unless the user asks for a batch.
114
+
115
+ Suggested interaction flow:
116
+
117
+ 1. Identify the target network, usually `sepolia` for testing.
118
+ 2. Identify whether a channel already exists.
119
+ 3. Identify the sender and recipient wallets or private-key environment variables.
120
+ 4. Run `--doctor`.
121
+ 5. Run `list-local-wallets` and relevant metadata or balance checks.
122
+ 6. If needed, guide the user through `create-channel`, `deposit-bridge`, `join-channel`, `deposit-channel`, and
123
+ `mint-notes`.
124
+ 7. For a private transfer, select available note IDs from `get-my-notes`, find the recipient L2 address from
125
+ `get-my-wallet-meta`, then build `transfer-notes`.
126
+ 8. After transfer, guide the recipient to run `get-my-notes` to recover received notes from event logs.
127
+
128
+ Example style: if the user says, "ADDR6 sends 10 tokens privately to ADDR8", do not assume the required note exists.
129
+ First ask or check which channel and network to use, whether ADDR6 and ADDR8 are already joined, what the local wallet
130
+ names are, and whether ADDR6 has an unused note worth exactly 10 or notes that sum to 10. Then provide the next concrete
131
+ command.
132
+
57
133
  ## Artifacts
58
134
 
59
135
  Proof-backed commands require installed bridge, DApp, and Groth16 artifacts. Run `private-state-cli --install` before
@@ -71,7 +147,8 @@ Release order matters for npm publication. `@tokamak-private-dapps/common-librar
71
147
  ### What does this package install?
72
148
 
73
149
  It installs the `private-state-cli` terminal command and the local files needed by that command.
74
- It does not install bridge contracts, app contracts, or local deployment outputs.
150
+ It does not install bridge contracts, app contracts, or local deployment outputs. The `private-state-cli --install`
151
+ command provisions the local Tokamak zk-EVM and Groth16 runtime workspaces used by proof-backed commands.
75
152
 
76
153
  ### When should I run `private-state-cli --install`?
77
154
 
@@ -528,7 +528,9 @@
528
528
  { id: "get-my-bridge-fund", description: "Read the current bridge-vault balance.", fields: ["network", "privateKey", "alchemyApiKey"] },
529
529
  { id: "join-channel", description: "Bind the caller to a channel-specific L2 identity.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
530
530
  { id: "recover-wallet", description: "Rebuild the recoverable portion of a wallet.", fields: ["channelName", "password", "network", "privateKey", "alchemyApiKey"] },
531
- { id: "get-my-address", description: "Check whether a saved wallet matches on-chain registration.", fields: ["wallet", "password", "network"] },
531
+ { id: "get-my-wallet-meta", description: "Check whether a saved wallet matches on-chain registration.", fields: ["wallet", "password", "network"] },
532
+ { id: "get-my-l1-address", description: "Derive the L1 address for a private key.", fields: ["privateKey"] },
533
+ { id: "list-local-wallets", description: "List saved local wallet names that can be reused with --wallet.", fields: ["network", "channelName"] },
532
534
  { id: "deposit-channel", description: "Move bridged funds into the channel L2 accounting balance.", fields: ["wallet", "password", "network", "amount"] },
533
535
  { id: "withdraw-channel", description: "Move channel L2 balance back into the shared bridge vault.", fields: ["wallet", "password", "network", "amount"] },
534
536
  { id: "get-my-channel-fund", description: "Read the current channel L2 accounting balance.", fields: ["wallet", "password", "network"] },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamak-private-dapps/private-state-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Command-line client for the Tokamak private-state DApp.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "author": "Tokamak Network",
@@ -5,6 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import process from "node:process";
7
7
  import { spawnSync } from "node:child_process";
8
+ import { createRequire } from "node:module";
8
9
  import {
9
10
  createCipheriv,
10
11
  createDecipheriv,
@@ -46,6 +47,7 @@ import { deriveRpcUrl, resolveCliNetwork } from "@tokamak-private-dapps/common-l
46
47
  import {
47
48
  buildTokamakCliInvocation,
48
49
  resolveTokamakBlockInputConfig,
50
+ resolveTokamakCliPackageRoot,
49
51
  resolveTokamakCliResourceDir,
50
52
  resolveTokamakCliRuntimeRoot,
51
53
  } from "@tokamak-private-dapps/common-library/tokamak-runtime-paths";
@@ -80,12 +82,16 @@ import {
80
82
  walletNameForChannelAndAddress,
81
83
  } from "./lib/private-state-cli-shared.mjs";
82
84
 
85
+ const require = createRequire(import.meta.url);
83
86
  const defaultCommandCwd = process.cwd();
87
+ const privateStateCliPackageRoot = path.dirname(require.resolve("./package.json"));
84
88
  const workspaceRoot = path.resolve(os.homedir(), "tokamak-private-channels", "workspace");
85
89
  const tokamakCliInvocation = buildTokamakCliInvocation();
86
90
  const tokamakCliCommand = tokamakCliInvocation.command;
87
91
  const tokamakCliBaseArgs = tokamakCliInvocation.args;
88
92
  const flatDeploymentArtifactPathsByChainId = new Map();
93
+ const DOCKER_CUDA_PROBE_IMAGE = "nvidia/cuda:12.2.0-base-ubuntu22.04";
94
+ const DOCTOR_GPU_PROBE_TIMEOUT_MS = 120000;
89
95
 
90
96
  const abiCoder = AbiCoder.defaultAbiCoder();
91
97
  const erc20MetadataAbi = [
@@ -221,6 +227,24 @@ async function main() {
221
227
  return;
222
228
  }
223
229
 
230
+ if (args.command === "--doctor") {
231
+ assertDoctorArgs(args);
232
+ await handleDoctor({ args });
233
+ return;
234
+ }
235
+
236
+ if (args.command === "get-my-l1-address") {
237
+ assertGetMyL1AddressArgs(args);
238
+ handleGetMyL1Address({ args });
239
+ return;
240
+ }
241
+
242
+ if (args.command === "list-local-wallets") {
243
+ assertListLocalWalletsArgs(args);
244
+ handleListLocalWallets({ args });
245
+ return;
246
+ }
247
+
224
248
  const walletCommandHandlers = {
225
249
  "mint-notes": {
226
250
  assert: assertMintNotesArgs,
@@ -246,9 +270,9 @@ async function main() {
246
270
  assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "withdraw-channel"),
247
271
  run: ({ provider }) => handleGrothVaultMove({ args, provider, direction: "withdraw" }),
248
272
  },
249
- "get-my-address": {
250
- assert: assertGetMyAddressArgs,
251
- run: ({ provider }) => handleGetMyAddress({ args, provider }),
273
+ "get-my-wallet-meta": {
274
+ assert: assertGetMyWalletMetaArgs,
275
+ run: ({ provider }) => handleGetMyWalletMeta({ args, provider }),
252
276
  },
253
277
  "get-my-channel-fund": {
254
278
  assert: assertGetMyChannelFundArgs,
@@ -932,15 +956,26 @@ async function handleInstallZkEvm({ args }) {
932
956
  }
933
957
  run(tokamakCliCommand, installArgs);
934
958
  const tokamakRuntimeRoot = resolveTokamakCliRuntimeRoot();
959
+ const groth16Runtime = installGroth16RuntimeForPrivateState({
960
+ docker: Boolean(args.docker),
961
+ });
935
962
  const localDeploymentBaseRoot = args.includeLocalArtifacts ? process.cwd() : null;
936
963
  const deploymentArtifacts = await installPrivateStateCliArtifacts({
937
964
  dappName: PRIVATE_STATE_DAPP_LABEL,
938
965
  localDeploymentBaseRoot,
939
966
  });
967
+ const installManifest = writePrivateStateCliInstallManifest({
968
+ dockerRequested: Boolean(args.docker),
969
+ includeLocalArtifacts: Boolean(args.includeLocalArtifacts),
970
+ localDeploymentBaseRoot,
971
+ deploymentArtifacts,
972
+ groth16Runtime,
973
+ });
940
974
  printJson({
941
975
  action: "install",
942
976
  tokamakCli: tokamakCliBaseArgs[0],
943
977
  runtimeRoot: tokamakRuntimeRoot,
978
+ groth16Runtime,
944
979
  docker: Boolean(args.docker),
945
980
  includeLocalArtifacts: Boolean(args.includeLocalArtifacts),
946
981
  localDeploymentBaseRoot,
@@ -953,6 +988,7 @@ async function handleInstallZkEvm({ args }) {
953
988
  dappTimestamp: entry.dappTimestamp,
954
989
  artifactDir: entry.artifactDir,
955
990
  })),
991
+ installManifestPath: installManifest.manifestPath,
956
992
  });
957
993
  }
958
994
 
@@ -972,7 +1008,46 @@ async function handleUninstallZkEvm() {
972
1008
  });
973
1009
  }
974
1010
 
975
- async function handleGetMyAddress({ args, provider }) {
1011
+ async function handleDoctor() {
1012
+ const report = buildDoctorReport();
1013
+ printJson(report);
1014
+ if (!report.ok) {
1015
+ process.exitCode = 1;
1016
+ }
1017
+ }
1018
+
1019
+ function handleGetMyL1Address({ args }) {
1020
+ const privateKey = normalizePrivateKey(requireArg(args.privateKey, "--private-key"));
1021
+ const signer = new Wallet(privateKey);
1022
+ printJson({
1023
+ action: "get-my-l1-address",
1024
+ l1Address: signer.address,
1025
+ });
1026
+ }
1027
+
1028
+ function handleListLocalWallets({ args }) {
1029
+ const networkFilter = args.network ? requireNetworkName(args) : null;
1030
+ if (networkFilter) {
1031
+ resolveCliNetwork(networkFilter);
1032
+ }
1033
+ const channelFilter = args.channelName ? slugifyPathComponent(requireArg(args.channelName, "--channel-name")) : null;
1034
+ const wallets = listLocalWallets({
1035
+ networkFilter,
1036
+ channelFilter,
1037
+ });
1038
+
1039
+ printJson({
1040
+ action: "list-local-wallets",
1041
+ workspaceRoot,
1042
+ filters: {
1043
+ network: networkFilter,
1044
+ channelName: args.channelName ?? null,
1045
+ },
1046
+ wallets,
1047
+ });
1048
+ }
1049
+
1050
+ async function handleGetMyWalletMeta({ args, provider }) {
976
1051
  const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
977
1052
  const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
978
1053
  const context = await loadChannelContext({
@@ -990,7 +1065,7 @@ async function handleGetMyAddress({ args, provider }) {
990
1065
  === ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
991
1066
 
992
1067
  printJson({
993
- action: "get-my-address",
1068
+ action: "get-my-wallet-meta",
994
1069
  wallet: wallet.walletName,
995
1070
  network: walletMetadata.network,
996
1071
  channelName: walletMetadata.channelName,
@@ -4217,6 +4292,10 @@ function parseArgs(argv) {
4217
4292
  parsed.command = "--install";
4218
4293
  parsed.positional = ["--install"];
4219
4294
  }
4295
+ if (!parsed.command && parsed.doctor === true) {
4296
+ parsed.command = "--doctor";
4297
+ parsed.positional = ["--doctor"];
4298
+ }
4220
4299
  return parsed;
4221
4300
  }
4222
4301
 
@@ -4348,6 +4427,48 @@ function resolveWalletPathCandidates(walletName) {
4348
4427
  return candidates;
4349
4428
  }
4350
4429
 
4430
+ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
4431
+ if (!fs.existsSync(workspaceRoot)) {
4432
+ return [];
4433
+ }
4434
+
4435
+ const wallets = [];
4436
+ for (const networkEntry of fs.readdirSync(workspaceRoot, { withFileTypes: true })) {
4437
+ if (!networkEntry.isDirectory() || (networkFilter && networkEntry.name !== slugifyPathComponent(networkFilter))) {
4438
+ continue;
4439
+ }
4440
+ const networkDir = path.join(workspaceRoot, networkEntry.name);
4441
+ for (const channelEntry of fs.readdirSync(networkDir, { withFileTypes: true })) {
4442
+ if (!channelEntry.isDirectory() || (channelFilter && channelEntry.name !== channelFilter)) {
4443
+ continue;
4444
+ }
4445
+ const walletsDir = path.join(networkDir, channelEntry.name, "wallets");
4446
+ if (!fs.existsSync(walletsDir)) {
4447
+ continue;
4448
+ }
4449
+ for (const walletEntry of fs.readdirSync(walletsDir, { withFileTypes: true })) {
4450
+ if (!walletEntry.isDirectory()) {
4451
+ continue;
4452
+ }
4453
+ const walletDir = path.join(walletsDir, walletEntry.name);
4454
+ wallets.push({
4455
+ wallet: walletEntry.name,
4456
+ network: networkEntry.name,
4457
+ channelName: channelEntry.name,
4458
+ walletDir,
4459
+ metadataPath: walletMetadataPath(walletDir),
4460
+ hasMetadata: fs.existsSync(walletMetadataPath(walletDir)),
4461
+ hasEncryptedWallet: walletConfigExists(walletDir),
4462
+ });
4463
+ }
4464
+ }
4465
+ }
4466
+ return wallets.sort((left, right) =>
4467
+ [left.network, left.channelName, left.wallet].join("\0")
4468
+ .localeCompare([right.network, right.channelName, right.wallet].join("\0")),
4469
+ );
4470
+ }
4471
+
4351
4472
  function channelDataPath(workspaceDir) {
4352
4473
  return workspaceChannelDir(workspaceDir);
4353
4474
  }
@@ -4421,6 +4542,10 @@ function assertUninstallZkEvmArgs(args) {
4421
4542
  assertAllowedCommandKeys(args, "uninstall-zk-evm", new Set(["command", "positional"]), "no options");
4422
4543
  }
4423
4544
 
4545
+ function assertDoctorArgs(args) {
4546
+ assertAllowedCommandKeys(args, "--doctor", new Set(["command", "positional", "doctor"]), "no options");
4547
+ }
4548
+
4424
4549
  function assertMintNotesArgs(args) {
4425
4550
  requireArg(args.amounts, "--amounts");
4426
4551
  assertWalletPasswordArgs(args, "mint-notes", ["amounts"], "--wallet, --password, --network, and --amounts");
@@ -4533,8 +4658,33 @@ function assertJoinChannelArgs(args) {
4533
4658
  assertExplicitSignerPasswordCommandArgs(args, "join-channel");
4534
4659
  }
4535
4660
 
4536
- function assertGetMyAddressArgs(args) {
4537
- assertWalletPasswordArgs(args, "get-my-address", [], "--wallet, --password, and --network");
4661
+ function assertGetMyWalletMetaArgs(args) {
4662
+ assertWalletPasswordArgs(args, "get-my-wallet-meta", [], "--wallet, --password, and --network");
4663
+ }
4664
+
4665
+ function assertGetMyL1AddressArgs(args) {
4666
+ requireArg(args.privateKey, "--private-key");
4667
+ assertAllowedCommandKeys(
4668
+ args,
4669
+ "get-my-l1-address",
4670
+ new Set(["command", "positional", "privateKey"]),
4671
+ "--private-key",
4672
+ );
4673
+ }
4674
+
4675
+ function assertListLocalWalletsArgs(args) {
4676
+ if (args.network !== undefined) {
4677
+ requireNetworkName(args);
4678
+ }
4679
+ if (args.channelName !== undefined) {
4680
+ requireArg(args.channelName, "--channel-name");
4681
+ }
4682
+ assertAllowedCommandKeys(
4683
+ args,
4684
+ "list-local-wallets",
4685
+ new Set(["command", "positional", "network", "channelName"]),
4686
+ "optional --network and --channel-name",
4687
+ );
4538
4688
  }
4539
4689
 
4540
4690
  function assertWithdrawBridgeArgs(args) {
@@ -4596,13 +4746,16 @@ function printHelp() {
4596
4746
  console.log(`
4597
4747
  Commands:
4598
4748
  --install [--docker] [--include-local-artifacts]
4599
- Install the Tokamak zk-EVM CLI runtime workspace and private-state deployment artifacts
4600
- Use --docker on Linux to forward tokamak-cli --install --docker
4749
+ Install the Tokamak zk-EVM CLI runtime, Groth16 runtime, and private-state deployment artifacts
4750
+ Use --docker on Linux to forward Docker mode to the Tokamak zk-EVM and Groth16 runtimes
4601
4751
  Use --include-local-artifacts to also install local deployment/ artifacts from the current working directory
4602
4752
 
4603
4753
  uninstall-zk-evm
4604
4754
  Remove the Tokamak zk-EVM CLI runtime workspace
4605
4755
 
4756
+ --doctor
4757
+ Check private-state CLI package versions, runtime install state, Docker mode, CUDA mode, and deployment artifacts
4758
+
4606
4759
  create-channel --channel-name <NAME> --join-fee <TOKENS> --network <NAME> --private-key <HEX> --alchemy-api-key <KEY>
4607
4760
  Create a bridge channel and initialize its workspace
4608
4761
 
@@ -4624,9 +4777,15 @@ Commands:
4624
4777
  join-channel --channel-name <NAME> --password <PASSWORD> --network <NAME> --private-key <HEX> --alchemy-api-key <KEY>
4625
4778
  Pay the channel join fee and bind a wallet to a channel-specific L2 identity
4626
4779
 
4627
- get-my-address --wallet <NAME> --password <PASSWORD> --network <NAME>
4780
+ get-my-wallet-meta --wallet <NAME> --password <PASSWORD> --network <NAME>
4628
4781
  Check whether a wallet matches the on-chain channel registration
4629
4782
 
4783
+ get-my-l1-address --private-key <HEX>
4784
+ Derive the L1 address for a private key
4785
+
4786
+ list-local-wallets [--network <NAME>] [--channel-name <NAME>]
4787
+ List saved local wallet names that can be reused with --wallet
4788
+
4630
4789
  deposit-channel --wallet <NAME> --password <PASSWORD> --network <NAME> --amount <TOKENS>
4631
4790
  Move bridged funds into the channel L2 accounting balance
4632
4791
 
@@ -4882,6 +5041,396 @@ function privateStateCliArtifactPaths(cacheBaseRoot = resolveArtifactCacheBaseRo
4882
5041
  };
4883
5042
  }
4884
5043
 
5044
+ function privateStateCliInstallManifestPath(cacheBaseRoot = resolveArtifactCacheBaseRoot()) {
5045
+ return path.join(privateStateCliArtifactRoot(cacheBaseRoot), "install-manifest.json");
5046
+ }
5047
+
5048
+ function writePrivateStateCliInstallManifest({
5049
+ dockerRequested,
5050
+ includeLocalArtifacts,
5051
+ localDeploymentBaseRoot,
5052
+ deploymentArtifacts,
5053
+ groth16Runtime,
5054
+ }) {
5055
+ const manifestPath = privateStateCliInstallManifestPath(deploymentArtifacts.cacheBaseRoot);
5056
+ const manifest = {
5057
+ installedAt: new Date().toISOString(),
5058
+ package: summarizePackageReport(readPackageReport({
5059
+ name: "@tokamak-private-dapps/private-state-cli",
5060
+ packageJsonPath: path.join(privateStateCliPackageRoot, "package.json"),
5061
+ })),
5062
+ dependencies: collectDependencyPackageReports().map(summarizePackageReport),
5063
+ install: {
5064
+ dockerRequested,
5065
+ includeLocalArtifacts,
5066
+ localDeploymentBaseRoot,
5067
+ artifactCacheRoot: deploymentArtifacts.cacheBaseRoot,
5068
+ groth16Runtime,
5069
+ installedDeploymentArtifacts: deploymentArtifacts.installed.map((entry) => ({
5070
+ chainId: entry.chainId,
5071
+ source: entry.source,
5072
+ bridgeTimestamp: entry.bridgeTimestamp,
5073
+ dappTimestamp: entry.dappTimestamp,
5074
+ })),
5075
+ },
5076
+ };
5077
+ writeJson(manifestPath, manifest);
5078
+ return { manifestPath, manifest };
5079
+ }
5080
+
5081
+ function summarizePackageReport(report) {
5082
+ return {
5083
+ name: report.name,
5084
+ version: report.version,
5085
+ };
5086
+ }
5087
+
5088
+ function buildDoctorReport() {
5089
+ const cacheBaseRoot = resolveArtifactCacheBaseRoot();
5090
+ const installManifestPath = privateStateCliInstallManifestPath(cacheBaseRoot);
5091
+ const installManifest = readJsonIfExists(installManifestPath);
5092
+ const dependencyReports = collectDependencyPackageReports(installManifest);
5093
+ const tokamakCli = inspectTokamakCliRuntime();
5094
+ const groth16Runtime = inspectGroth16Runtime();
5095
+ const gpuDockerReadiness = inspectGpuDockerReadiness(tokamakCli);
5096
+ const checks = [
5097
+ {
5098
+ name: "dependency package versions",
5099
+ ok: dependencyReports.every((entry) => entry.ok),
5100
+ details: dependencyReports.map((entry) => ({
5101
+ name: entry.name,
5102
+ currentVersion: entry.version,
5103
+ installVersion: entry.installVersion,
5104
+ ok: entry.ok,
5105
+ error: entry.error,
5106
+ })),
5107
+ },
5108
+ {
5109
+ name: "tokamak zk-evm runtime",
5110
+ ok: tokamakCli.installed,
5111
+ details: {
5112
+ doctorStatus: tokamakCli.doctor.status,
5113
+ runtimeRoot: tokamakCli.runtimeRoot,
5114
+ installations: tokamakCli.installations.map(({ platform, installMode, packageVersion, docker }) => ({
5115
+ platform,
5116
+ installMode,
5117
+ packageVersion,
5118
+ dockerEnvironment: docker?.dockerEnvironment ?? null,
5119
+ useGpus: docker?.useGpus ?? null,
5120
+ })),
5121
+ },
5122
+ },
5123
+ {
5124
+ name: "tokamak docker gpu readiness",
5125
+ ok: gpuDockerReadiness.ok,
5126
+ details: {
5127
+ expectedUseGpus: gpuDockerReadiness.expectedUseGpus,
5128
+ liveUseGpus: gpuDockerReadiness.liveUseGpus,
5129
+ mismatch: gpuDockerReadiness.mismatch,
5130
+ mismatchError: gpuDockerReadiness.mismatchError,
5131
+ hostNvidiaSmi: summarizeProbeResult(gpuDockerReadiness.hostNvidiaSmi),
5132
+ dockerNvidiaSmi: summarizeProbeResult(gpuDockerReadiness.dockerNvidiaSmi),
5133
+ },
5134
+ },
5135
+ {
5136
+ name: "groth16 runtime",
5137
+ ok: groth16Runtime.installed,
5138
+ details: {
5139
+ packageRoot: groth16Runtime.packageRoot,
5140
+ workspaceRoot: groth16Runtime.workspaceRoot,
5141
+ doctorStatus: groth16Runtime.doctor.status,
5142
+ checks: groth16Runtime.checks,
5143
+ },
5144
+ },
5145
+ ];
5146
+
5147
+ return {
5148
+ action: "doctor",
5149
+ ok: checks.every((check) => check.ok),
5150
+ generatedAt: new Date().toISOString(),
5151
+ package: readPackageReport({
5152
+ name: "@tokamak-private-dapps/private-state-cli",
5153
+ packageJsonPath: path.join(privateStateCliPackageRoot, "package.json"),
5154
+ }),
5155
+ installManifest: {
5156
+ path: installManifestPath,
5157
+ exists: Boolean(installManifest),
5158
+ installedAt: installManifest?.installedAt ?? null,
5159
+ dockerRequested: installManifest?.install?.dockerRequested ?? null,
5160
+ includeLocalArtifacts: installManifest?.install?.includeLocalArtifacts ?? null,
5161
+ },
5162
+ dependencies: dependencyReports,
5163
+ tokamakCli,
5164
+ groth16Runtime,
5165
+ gpuDockerReadiness,
5166
+ checks,
5167
+ };
5168
+ }
5169
+
5170
+ function installGroth16RuntimeForPrivateState({ docker }) {
5171
+ const packageRoot = resolveGroth16PackageRoot();
5172
+ const entryPath = resolveGroth16CliEntryPath(packageRoot);
5173
+ const args = [entryPath, "--install"];
5174
+ if (docker) {
5175
+ args.push("--docker");
5176
+ }
5177
+ run(process.execPath, args, { cwd: packageRoot });
5178
+ const runtime = inspectGroth16Runtime();
5179
+ expect(runtime.installed, "Groth16 runtime install completed, but tokamak-groth16 --doctor still reports an unhealthy runtime.");
5180
+ return runtime;
5181
+ }
5182
+
5183
+ function collectDependencyPackageReports(installManifest = null) {
5184
+ const installVersions = new Map(
5185
+ Array.isArray(installManifest?.dependencies)
5186
+ ? installManifest.dependencies.map((entry) => [entry.name, entry.version])
5187
+ : [],
5188
+ );
5189
+ const targets = [
5190
+ {
5191
+ name: "@tokamak-zk-evm/cli",
5192
+ packageJsonPath: path.join(resolveTokamakCliPackageRoot(), "package.json"),
5193
+ },
5194
+ {
5195
+ name: "@tokamak-private-dapps/groth16",
5196
+ resolveTarget: "@tokamak-private-dapps/groth16/public-drive-crs",
5197
+ },
5198
+ {
5199
+ name: "@tokamak-private-dapps/common-library",
5200
+ resolveTarget: "@tokamak-private-dapps/common-library/artifact-cache",
5201
+ },
5202
+ { name: "tokamak-l2js", resolveTarget: "tokamak-l2js" },
5203
+ ];
5204
+
5205
+ return targets.map((target) => {
5206
+ const report = readPackageReport(target);
5207
+ const installVersion = installVersions.get(report.name) ?? null;
5208
+ return {
5209
+ ...report,
5210
+ installVersion,
5211
+ ok: Boolean(report.version) && (installVersion === null || installVersion === report.version),
5212
+ };
5213
+ });
5214
+ }
5215
+
5216
+ function readPackageReport({ name, packageJsonPath = null, resolveTarget = null }) {
5217
+ try {
5218
+ const resolvedPackageJsonPath = packageJsonPath
5219
+ ? path.resolve(packageJsonPath)
5220
+ : findPackageJsonForName(path.dirname(require.resolve(resolveTarget ?? name)), name);
5221
+ const packageJson = readJson(resolvedPackageJsonPath);
5222
+ return {
5223
+ name: packageJson.name ?? name,
5224
+ version: packageJson.version ?? null,
5225
+ packageRoot: path.dirname(resolvedPackageJsonPath),
5226
+ error: null,
5227
+ };
5228
+ } catch (error) {
5229
+ return {
5230
+ name,
5231
+ version: null,
5232
+ packageRoot: null,
5233
+ error: error.message,
5234
+ ok: false,
5235
+ };
5236
+ }
5237
+ }
5238
+
5239
+ function findPackageJsonForName(startDir, expectedName) {
5240
+ let current = path.resolve(startDir);
5241
+ while (current !== path.dirname(current)) {
5242
+ const candidate = path.join(current, "package.json");
5243
+ if (fs.existsSync(candidate)) {
5244
+ const packageJson = readJson(candidate);
5245
+ if (packageJson.name === expectedName) {
5246
+ return candidate;
5247
+ }
5248
+ }
5249
+ current = path.dirname(current);
5250
+ }
5251
+ throw new Error(`Cannot locate package.json for ${expectedName} above ${startDir}.`);
5252
+ }
5253
+
5254
+ function resolveGroth16PackageRoot() {
5255
+ const publicDriveCrsPath = require.resolve("@tokamak-private-dapps/groth16/public-drive-crs");
5256
+ return path.dirname(findPackageJsonForName(path.dirname(publicDriveCrsPath), "@tokamak-private-dapps/groth16"));
5257
+ }
5258
+
5259
+ function resolveGroth16CliEntryPath(packageRoot = resolveGroth16PackageRoot()) {
5260
+ return path.join(packageRoot, "cli", "tokamak-groth16-cli.mjs");
5261
+ }
5262
+
5263
+ function inspectGroth16Runtime() {
5264
+ const packageRoot = resolveGroth16PackageRoot();
5265
+ const entryPath = resolveGroth16CliEntryPath(packageRoot);
5266
+ const doctor = runCaptured(process.execPath, [entryPath, "--doctor", "--verbose"], { cwd: packageRoot });
5267
+ const stdout = stripAnsi(doctor.stdout).trim();
5268
+ const stderr = stripAnsi(doctor.stderr).trim();
5269
+ const report = parseJsonReport(stdout);
5270
+ return {
5271
+ installed: doctor.status === 0 && report?.ok === true,
5272
+ packageRoot,
5273
+ workspaceRoot: report?.workspaceRoot ?? null,
5274
+ checks: report?.checks ?? [],
5275
+ doctor: {
5276
+ status: doctor.status,
5277
+ stdout,
5278
+ stderr,
5279
+ },
5280
+ };
5281
+ }
5282
+
5283
+ function inspectTokamakCliRuntime() {
5284
+ const doctor = runCaptured(tokamakCliCommand, [...tokamakCliBaseArgs, "--doctor"], {
5285
+ cwd: resolveTokamakCliPackageRoot(),
5286
+ });
5287
+ const doctorOutput = stripAnsi(`${doctor.stdout}${doctor.stderr}`);
5288
+ const runtimeRoot = parseRuntimeRootFromTokamakDoctorOutput(doctorOutput);
5289
+ const cacheRoot = resolveTokamakCliCacheRoot();
5290
+ const installations = readTokamakCliInstallations(cacheRoot);
5291
+ const dockerModeInstalled = installations.some((entry) => entry.installMode === "docker" || entry.docker);
5292
+ const cudaCompatible = installations.some((entry) => entry.docker?.useGpus === true);
5293
+
5294
+ return {
5295
+ installed: doctor.status === 0 || installations.length > 0,
5296
+ packageRoot: resolveTokamakCliPackageRoot(),
5297
+ cacheRoot,
5298
+ runtimeRoot,
5299
+ packageVersion: readPackageReport({
5300
+ name: "@tokamak-zk-evm/cli",
5301
+ packageJsonPath: path.join(resolveTokamakCliPackageRoot(), "package.json"),
5302
+ }).version,
5303
+ dockerModeInstalled,
5304
+ cudaCompatible,
5305
+ doctor: {
5306
+ status: doctor.status,
5307
+ stdout: stripAnsi(doctor.stdout).trim(),
5308
+ stderr: stripAnsi(doctor.stderr).trim(),
5309
+ },
5310
+ installations,
5311
+ };
5312
+ }
5313
+
5314
+ function inspectGpuDockerReadiness(tokamakCli) {
5315
+ const hostNvidiaSmi = runProbe("nvidia-smi", ["--query-gpu=name,driver_version", "--format=csv,noheader"]);
5316
+ const dockerNvidiaSmi = runProbe("docker", [
5317
+ "run",
5318
+ "--rm",
5319
+ "--gpus",
5320
+ "all",
5321
+ DOCKER_CUDA_PROBE_IMAGE,
5322
+ "nvidia-smi",
5323
+ ]);
5324
+ const expectedUseGpus = Boolean(tokamakCli.cudaCompatible);
5325
+ const liveUseGpus = hostNvidiaSmi.ok && dockerNvidiaSmi.ok;
5326
+ const mismatch = expectedUseGpus !== liveUseGpus;
5327
+ return {
5328
+ ok: !mismatch,
5329
+ expectedUseGpus,
5330
+ liveUseGpus,
5331
+ mismatch,
5332
+ mismatchError: mismatch
5333
+ ? [
5334
+ "Tokamak CLI Docker GPU metadata does not match live NVIDIA/Docker GPU probes.",
5335
+ `metadata useGpus=${expectedUseGpus}; live useGpus=${liveUseGpus}.`,
5336
+ ].join(" ")
5337
+ : null,
5338
+ probeImage: DOCKER_CUDA_PROBE_IMAGE,
5339
+ hostNvidiaSmi,
5340
+ dockerNvidiaSmi,
5341
+ };
5342
+ }
5343
+
5344
+ function runProbe(command, args) {
5345
+ const result = spawnSync(command, args, {
5346
+ encoding: "utf8",
5347
+ timeout: DOCTOR_GPU_PROBE_TIMEOUT_MS,
5348
+ stdio: ["ignore", "pipe", "pipe"],
5349
+ });
5350
+ return {
5351
+ command,
5352
+ args,
5353
+ ok: !result.error && result.status === 0,
5354
+ status: result.status,
5355
+ signal: result.signal,
5356
+ error: result.error ? result.error.message : null,
5357
+ stdout: stripAnsi(result.stdout ?? "").trim(),
5358
+ stderr: stripAnsi(result.stderr ?? "").trim(),
5359
+ timedOut: result.error?.code === "ETIMEDOUT",
5360
+ };
5361
+ }
5362
+
5363
+ function summarizeProbeResult(result) {
5364
+ return {
5365
+ command: [result.command, ...result.args].join(" "),
5366
+ ok: result.ok,
5367
+ status: result.status,
5368
+ signal: result.signal,
5369
+ error: result.error,
5370
+ timedOut: result.timedOut,
5371
+ stdout: truncateText(result.stdout, 2000),
5372
+ stderr: truncateText(result.stderr, 2000),
5373
+ };
5374
+ }
5375
+
5376
+ function truncateText(value, maxLength) {
5377
+ const text = String(value ?? "");
5378
+ if (text.length <= maxLength) {
5379
+ return text;
5380
+ }
5381
+ return `${text.slice(0, maxLength)}...`;
5382
+ }
5383
+
5384
+ function parseJsonReport(value) {
5385
+ try {
5386
+ return JSON.parse(value);
5387
+ } catch {
5388
+ return null;
5389
+ }
5390
+ }
5391
+
5392
+ function resolveTokamakCliCacheRoot() {
5393
+ return path.resolve(process.env.TOKAMAK_ZKEVM_CLI_CACHE_DIR ?? path.join(os.homedir(), ".tokamak-zk-evm"));
5394
+ }
5395
+
5396
+ function readTokamakCliInstallations(cacheRoot) {
5397
+ if (!fs.existsSync(cacheRoot)) {
5398
+ return [];
5399
+ }
5400
+ return fs.readdirSync(cacheRoot, { withFileTypes: true })
5401
+ .filter((entry) => entry.isDirectory())
5402
+ .map((entry) => {
5403
+ const platformDir = path.join(cacheRoot, entry.name);
5404
+ const statePath = path.join(platformDir, "installation.json");
5405
+ if (!fs.existsSync(statePath)) {
5406
+ return null;
5407
+ }
5408
+ const state = readJsonIfExists(statePath);
5409
+ const dockerBootstrapPath = path.join(platformDir, "docker", "bootstrap.json");
5410
+ const docker = readJsonIfExists(dockerBootstrapPath);
5411
+ return {
5412
+ platform: entry.name,
5413
+ statePath,
5414
+ runtimeRoot: path.join(platformDir, "runtime"),
5415
+ installMode: state?.installMode ?? (docker ? "docker" : null),
5416
+ packageVersion: state?.packageVersion ?? docker?.packageVersion ?? null,
5417
+ installedAt: state?.installedAt ?? null,
5418
+ dockerBootstrapPath,
5419
+ docker,
5420
+ };
5421
+ })
5422
+ .filter(Boolean);
5423
+ }
5424
+
5425
+ function parseRuntimeRootFromTokamakDoctorOutput(output) {
5426
+ const match = String(output ?? "").match(/^\[ ok \] Runtime workspace:\s*(.+)$/m);
5427
+ return match ? path.resolve(match[1].trim()) : null;
5428
+ }
5429
+
5430
+ function stripAnsi(value) {
5431
+ return String(value ?? "").replace(/\u001b\[[0-9;]*m/g, "");
5432
+ }
5433
+
4885
5434
  async function installPrivateStateCliArtifacts({
4886
5435
  dappName,
4887
5436
  indexFileId = process.env.PRIVATE_STATE_DRIVE_ARTIFACT_INDEX_FILE_ID