@tokamak-private-dapps/private-state-cli 0.1.2 → 0.1.4

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.4 - 2026-04-28
4
+
5
+ - Paced chunked log recovery queries at five requests per second to avoid RPC throughput bursts.
6
+ - Combined channel manager recovery log scans and filtered wallet note recovery scans to reduce RPC usage.
7
+
8
+ ## 0.1.3 - 2026-04-28
9
+
10
+ - Installed the Groth16 runtime during `private-state-cli --install` and reported Groth16 readiness from `--doctor`.
11
+ - 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.
12
+ - Renamed `get-my-address` to `get-my-wallet-meta`, added `get-my-l1-address`, and added `list-local-wallets`.
13
+ - Documented the private-state CLI helper commands, common flow examples, and LLM agent guidance.
14
+
3
15
  ## 0.1.2 - 2026-04-28
4
16
 
5
17
  - Added `private-state-cli --doctor` to report CLI and install-time dependency versions through `tokamak-l2js`.
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
@@ -35,7 +36,7 @@ private-state-cli --doctor
35
36
 
36
37
  ## Commands
37
38
 
38
- The normal private-state flow is:
39
+ A common private-state flow is:
39
40
 
40
41
  1. `create-channel`
41
42
  2. `deposit-bridge`
@@ -52,7 +53,21 @@ Use `private-state-cli --help` for the full command list and required options.
52
53
 
53
54
  `private-state-cli --doctor` reports the CLI package version, dependency versions recorded by the last
54
55
  `private-state-cli --install`, current dependency versions through `tokamak-l2js`, and Tokamak zk-EVM runtime
55
- install mode, Docker mode, and CUDA runtime metadata.
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.
56
71
 
57
72
  ## Workspace
58
73
 
@@ -64,6 +79,57 @@ The CLI stores user workspaces under:
64
79
 
65
80
  Wallet data is encrypted with the password supplied to `join-channel` or `recover-wallet`.
66
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
+
67
133
  ## Artifacts
68
134
 
69
135
  Proof-backed commands require installed bridge, DApp, and Groth16 artifacts. Run `private-state-cli --install` before
@@ -81,7 +147,8 @@ Release order matters for npm publication. `@tokamak-private-dapps/common-librar
81
147
  ### What does this package install?
82
148
 
83
149
  It installs the `private-state-cli` terminal command and the local files needed by that command.
84
- 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.
85
152
 
86
153
  ### When should I run `private-state-cli --install`?
87
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.2",
3
+ "version": "0.1.4",
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",
@@ -90,6 +90,8 @@ const tokamakCliInvocation = buildTokamakCliInvocation();
90
90
  const tokamakCliCommand = tokamakCliInvocation.command;
91
91
  const tokamakCliBaseArgs = tokamakCliInvocation.args;
92
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;
93
95
 
94
96
  const abiCoder = AbiCoder.defaultAbiCoder();
95
97
  const erc20MetadataAbi = [
@@ -150,6 +152,9 @@ const JUBJUB_D = jubjub.CURVE.d;
150
152
  const BLS12_381_SCALAR_FIELD_MODULUS =
151
153
  hexToBigInt(addHexPrefix("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"));
152
154
  const DEFAULT_LOG_CHUNK_SIZE = 2000;
155
+ const DEFAULT_LOG_REQUESTS_PER_SECOND = 5;
156
+ const LOG_REQUEST_INTERVAL_MS = Math.ceil(1000 / DEFAULT_LOG_REQUESTS_PER_SECOND);
157
+ let lastLogRequestStartedAtMs = 0;
153
158
 
154
159
  async function prepareDeploymentArtifacts(chainId) {
155
160
  const normalizedChainId = Number(chainId);
@@ -231,6 +236,18 @@ async function main() {
231
236
  return;
232
237
  }
233
238
 
239
+ if (args.command === "get-my-l1-address") {
240
+ assertGetMyL1AddressArgs(args);
241
+ handleGetMyL1Address({ args });
242
+ return;
243
+ }
244
+
245
+ if (args.command === "list-local-wallets") {
246
+ assertListLocalWalletsArgs(args);
247
+ handleListLocalWallets({ args });
248
+ return;
249
+ }
250
+
234
251
  const walletCommandHandlers = {
235
252
  "mint-notes": {
236
253
  assert: assertMintNotesArgs,
@@ -256,9 +273,9 @@ async function main() {
256
273
  assert: (parsedArgs) => assertWalletChannelMoveArgs(parsedArgs, "withdraw-channel"),
257
274
  run: ({ provider }) => handleGrothVaultMove({ args, provider, direction: "withdraw" }),
258
275
  },
259
- "get-my-address": {
260
- assert: assertGetMyAddressArgs,
261
- run: ({ provider }) => handleGetMyAddress({ args, provider }),
276
+ "get-my-wallet-meta": {
277
+ assert: assertGetMyWalletMetaArgs,
278
+ run: ({ provider }) => handleGetMyWalletMeta({ args, provider }),
262
279
  },
263
280
  "get-my-channel-fund": {
264
281
  assert: assertGetMyChannelFundArgs,
@@ -942,6 +959,9 @@ async function handleInstallZkEvm({ args }) {
942
959
  }
943
960
  run(tokamakCliCommand, installArgs);
944
961
  const tokamakRuntimeRoot = resolveTokamakCliRuntimeRoot();
962
+ const groth16Runtime = installGroth16RuntimeForPrivateState({
963
+ docker: Boolean(args.docker),
964
+ });
945
965
  const localDeploymentBaseRoot = args.includeLocalArtifacts ? process.cwd() : null;
946
966
  const deploymentArtifacts = await installPrivateStateCliArtifacts({
947
967
  dappName: PRIVATE_STATE_DAPP_LABEL,
@@ -952,11 +972,13 @@ async function handleInstallZkEvm({ args }) {
952
972
  includeLocalArtifacts: Boolean(args.includeLocalArtifacts),
953
973
  localDeploymentBaseRoot,
954
974
  deploymentArtifacts,
975
+ groth16Runtime,
955
976
  });
956
977
  printJson({
957
978
  action: "install",
958
979
  tokamakCli: tokamakCliBaseArgs[0],
959
980
  runtimeRoot: tokamakRuntimeRoot,
981
+ groth16Runtime,
960
982
  docker: Boolean(args.docker),
961
983
  includeLocalArtifacts: Boolean(args.includeLocalArtifacts),
962
984
  localDeploymentBaseRoot,
@@ -997,7 +1019,38 @@ async function handleDoctor() {
997
1019
  }
998
1020
  }
999
1021
 
1000
- async function handleGetMyAddress({ args, provider }) {
1022
+ function handleGetMyL1Address({ args }) {
1023
+ const privateKey = normalizePrivateKey(requireArg(args.privateKey, "--private-key"));
1024
+ const signer = new Wallet(privateKey);
1025
+ printJson({
1026
+ action: "get-my-l1-address",
1027
+ l1Address: signer.address,
1028
+ });
1029
+ }
1030
+
1031
+ function handleListLocalWallets({ args }) {
1032
+ const networkFilter = args.network ? requireNetworkName(args) : null;
1033
+ if (networkFilter) {
1034
+ resolveCliNetwork(networkFilter);
1035
+ }
1036
+ const channelFilter = args.channelName ? slugifyPathComponent(requireArg(args.channelName, "--channel-name")) : null;
1037
+ const wallets = listLocalWallets({
1038
+ networkFilter,
1039
+ channelFilter,
1040
+ });
1041
+
1042
+ printJson({
1043
+ action: "list-local-wallets",
1044
+ workspaceRoot,
1045
+ filters: {
1046
+ network: networkFilter,
1047
+ channelName: args.channelName ?? null,
1048
+ },
1049
+ wallets,
1050
+ });
1051
+ }
1052
+
1053
+ async function handleGetMyWalletMeta({ args, provider }) {
1001
1054
  const { wallet, walletMetadata } = loadUnlockedWalletWithMetadata(args);
1002
1055
  const { signer, l2Identity } = restoreWalletParticipant(wallet, provider);
1003
1056
  const context = await loadChannelContext({
@@ -1015,7 +1068,7 @@ async function handleGetMyAddress({ args, provider }) {
1015
1068
  === ethers.toBigInt(normalizeBytes32Hex(expectedStorageKey));
1016
1069
 
1017
1070
  printJson({
1018
- action: "get-my-address",
1071
+ action: "get-my-wallet-meta",
1019
1072
  wallet: wallet.walletName,
1020
1073
  network: walletMetadata.network,
1021
1074
  channelName: walletMetadata.channelName,
@@ -1980,6 +2033,7 @@ async function recoverDeliveredNotesFromEventLogs({
1980
2033
  const nullifierUsedSlot = ethers.toBigInt(findStorageSlot(storageLayoutManifest, "PrivateStateController", "nullifierUsed"));
1981
2034
  const observedLogs = await fetchLogsChunked(provider, {
1982
2035
  address: context.workspace.channelManager,
2036
+ topics: [NOTE_VALUE_ENCRYPTED_TOPIC],
1983
2037
  fromBlock: scanStartBlock,
1984
2038
  toBlock: latestBlock,
1985
2039
  });
@@ -3804,30 +3858,39 @@ async function reconstructChannelSnapshot({
3804
3858
  provider,
3805
3859
  );
3806
3860
  const latestBlock = await provider.getBlockNumber();
3807
- const rootEvents = await queryContractEventsChunked({
3808
- contract: channelManager,
3809
- eventName: "CurrentRootVectorObserved",
3861
+ const currentRootVectorObservedTopic =
3862
+ normalizeBytes32Hex(channelManager.interface.getEvent("CurrentRootVectorObserved").topicHash);
3863
+ const channelManagerLogs = await fetchLogsChunked(provider, {
3864
+ address: channelInfo.manager,
3865
+ topics: [[
3866
+ currentRootVectorObservedTopic,
3867
+ CONTROLLER_STORAGE_KEY_OBSERVED_TOPIC,
3868
+ VAULT_STORAGE_WRITE_OBSERVED_TOPIC,
3869
+ ]],
3810
3870
  fromBlock: genesisBlockNumber,
3811
3871
  toBlock: latestBlock,
3812
3872
  });
3873
+ const channelManagerEvents = channelManagerLogs.map((log) => {
3874
+ const topic0 = log.topics[0] ? normalizeBytes32Hex(log.topics[0]) : null;
3875
+ if (topic0 !== null && ethers.toBigInt(topic0) === ethers.toBigInt(currentRootVectorObservedTopic)) {
3876
+ const parsed = channelManager.interface.parseLog(log);
3877
+ return {
3878
+ ...log,
3879
+ args: parsed.args,
3880
+ fragment: parsed.fragment,
3881
+ };
3882
+ }
3883
+ return log;
3884
+ });
3813
3885
  const vaultStorageWriteEvents = await queryContractEventsChunked({
3814
3886
  contract: bridgeTokenVault,
3815
3887
  eventName: "StorageWriteObserved",
3816
3888
  fromBlock: genesisBlockNumber,
3817
3889
  toBlock: latestBlock,
3818
3890
  });
3819
- const observedStorageLogs = await fetchLogsChunked(provider, {
3820
- address: channelInfo.manager,
3821
- topics: [[
3822
- CONTROLLER_STORAGE_KEY_OBSERVED_TOPIC,
3823
- VAULT_STORAGE_WRITE_OBSERVED_TOPIC,
3824
- ]],
3825
- fromBlock: genesisBlockNumber,
3826
- toBlock: latestBlock,
3827
- });
3828
3891
 
3829
3892
  const groupedEvents = new Map();
3830
- for (const event of [...rootEvents, ...vaultStorageWriteEvents, ...observedStorageLogs]) {
3893
+ for (const event of [...channelManagerEvents, ...vaultStorageWriteEvents]) {
3831
3894
  const key = event.transactionHash;
3832
3895
  const group = groupedEvents.get(key) ?? [];
3833
3896
  group.push(event);
@@ -3950,6 +4013,7 @@ async function fetchLogsChunked(provider, {
3950
4013
  while (cursor <= resolvedToBlock) {
3951
4014
  const chunkToBlock = Math.min(resolvedToBlock, cursor + chunkSize - 1);
3952
4015
  try {
4016
+ await throttleLogRequest();
3953
4017
  const logs = await provider.getLogs({
3954
4018
  address,
3955
4019
  topics,
@@ -3959,6 +4023,12 @@ async function fetchLogsChunked(provider, {
3959
4023
  aggregatedLogs.push(...logs);
3960
4024
  cursor = chunkToBlock + 1;
3961
4025
  } catch (error) {
4026
+ if (isRateLimitError(error)) {
4027
+ throw new Error(
4028
+ `RPC log query rate limit exceeded. Log chunk requests are paced at ${DEFAULT_LOG_REQUESTS_PER_SECOND} requests per second.`,
4029
+ { cause: error },
4030
+ );
4031
+ }
3962
4032
  const suggestedChunkSize = deriveRecommendedLogChunkSize(error, chunkSize);
3963
4033
  if (suggestedChunkSize >= chunkSize) {
3964
4034
  throw error;
@@ -3970,6 +4040,31 @@ async function fetchLogsChunked(provider, {
3970
4040
  return aggregatedLogs;
3971
4041
  }
3972
4042
 
4043
+ async function throttleLogRequest() {
4044
+ const elapsedMs = Date.now() - lastLogRequestStartedAtMs;
4045
+ if (elapsedMs < LOG_REQUEST_INTERVAL_MS) {
4046
+ await sleep(LOG_REQUEST_INTERVAL_MS - elapsedMs);
4047
+ }
4048
+ lastLogRequestStartedAtMs = Date.now();
4049
+ }
4050
+
4051
+ function sleep(ms) {
4052
+ return new Promise((resolve) => setTimeout(resolve, ms));
4053
+ }
4054
+
4055
+ function isRateLimitError(error) {
4056
+ const serializedError = [
4057
+ error?.code,
4058
+ error?.status,
4059
+ error?.message,
4060
+ error?.shortMessage,
4061
+ error?.info?.responseStatus,
4062
+ error?.info?.responseBody,
4063
+ ].filter((value) => value !== undefined && value !== null).join("\n");
4064
+
4065
+ return /\b429\b|too many requests|rate limit|compute units/i.test(serializedError);
4066
+ }
4067
+
3973
4068
  function deriveRecommendedLogChunkSize(error, currentChunkSize) {
3974
4069
  const serializedError = [
3975
4070
  error?.message,
@@ -4377,6 +4472,48 @@ function resolveWalletPathCandidates(walletName) {
4377
4472
  return candidates;
4378
4473
  }
4379
4474
 
4475
+ function listLocalWallets({ networkFilter = null, channelFilter = null } = {}) {
4476
+ if (!fs.existsSync(workspaceRoot)) {
4477
+ return [];
4478
+ }
4479
+
4480
+ const wallets = [];
4481
+ for (const networkEntry of fs.readdirSync(workspaceRoot, { withFileTypes: true })) {
4482
+ if (!networkEntry.isDirectory() || (networkFilter && networkEntry.name !== slugifyPathComponent(networkFilter))) {
4483
+ continue;
4484
+ }
4485
+ const networkDir = path.join(workspaceRoot, networkEntry.name);
4486
+ for (const channelEntry of fs.readdirSync(networkDir, { withFileTypes: true })) {
4487
+ if (!channelEntry.isDirectory() || (channelFilter && channelEntry.name !== channelFilter)) {
4488
+ continue;
4489
+ }
4490
+ const walletsDir = path.join(networkDir, channelEntry.name, "wallets");
4491
+ if (!fs.existsSync(walletsDir)) {
4492
+ continue;
4493
+ }
4494
+ for (const walletEntry of fs.readdirSync(walletsDir, { withFileTypes: true })) {
4495
+ if (!walletEntry.isDirectory()) {
4496
+ continue;
4497
+ }
4498
+ const walletDir = path.join(walletsDir, walletEntry.name);
4499
+ wallets.push({
4500
+ wallet: walletEntry.name,
4501
+ network: networkEntry.name,
4502
+ channelName: channelEntry.name,
4503
+ walletDir,
4504
+ metadataPath: walletMetadataPath(walletDir),
4505
+ hasMetadata: fs.existsSync(walletMetadataPath(walletDir)),
4506
+ hasEncryptedWallet: walletConfigExists(walletDir),
4507
+ });
4508
+ }
4509
+ }
4510
+ }
4511
+ return wallets.sort((left, right) =>
4512
+ [left.network, left.channelName, left.wallet].join("\0")
4513
+ .localeCompare([right.network, right.channelName, right.wallet].join("\0")),
4514
+ );
4515
+ }
4516
+
4380
4517
  function channelDataPath(workspaceDir) {
4381
4518
  return workspaceChannelDir(workspaceDir);
4382
4519
  }
@@ -4566,8 +4703,33 @@ function assertJoinChannelArgs(args) {
4566
4703
  assertExplicitSignerPasswordCommandArgs(args, "join-channel");
4567
4704
  }
4568
4705
 
4569
- function assertGetMyAddressArgs(args) {
4570
- assertWalletPasswordArgs(args, "get-my-address", [], "--wallet, --password, and --network");
4706
+ function assertGetMyWalletMetaArgs(args) {
4707
+ assertWalletPasswordArgs(args, "get-my-wallet-meta", [], "--wallet, --password, and --network");
4708
+ }
4709
+
4710
+ function assertGetMyL1AddressArgs(args) {
4711
+ requireArg(args.privateKey, "--private-key");
4712
+ assertAllowedCommandKeys(
4713
+ args,
4714
+ "get-my-l1-address",
4715
+ new Set(["command", "positional", "privateKey"]),
4716
+ "--private-key",
4717
+ );
4718
+ }
4719
+
4720
+ function assertListLocalWalletsArgs(args) {
4721
+ if (args.network !== undefined) {
4722
+ requireNetworkName(args);
4723
+ }
4724
+ if (args.channelName !== undefined) {
4725
+ requireArg(args.channelName, "--channel-name");
4726
+ }
4727
+ assertAllowedCommandKeys(
4728
+ args,
4729
+ "list-local-wallets",
4730
+ new Set(["command", "positional", "network", "channelName"]),
4731
+ "optional --network and --channel-name",
4732
+ );
4571
4733
  }
4572
4734
 
4573
4735
  function assertWithdrawBridgeArgs(args) {
@@ -4629,8 +4791,8 @@ function printHelp() {
4629
4791
  console.log(`
4630
4792
  Commands:
4631
4793
  --install [--docker] [--include-local-artifacts]
4632
- Install the Tokamak zk-EVM CLI runtime workspace and private-state deployment artifacts
4633
- Use --docker on Linux to forward tokamak-cli --install --docker
4794
+ Install the Tokamak zk-EVM CLI runtime, Groth16 runtime, and private-state deployment artifacts
4795
+ Use --docker on Linux to forward Docker mode to the Tokamak zk-EVM and Groth16 runtimes
4634
4796
  Use --include-local-artifacts to also install local deployment/ artifacts from the current working directory
4635
4797
 
4636
4798
  uninstall-zk-evm
@@ -4660,9 +4822,15 @@ Commands:
4660
4822
  join-channel --channel-name <NAME> --password <PASSWORD> --network <NAME> --private-key <HEX> --alchemy-api-key <KEY>
4661
4823
  Pay the channel join fee and bind a wallet to a channel-specific L2 identity
4662
4824
 
4663
- get-my-address --wallet <NAME> --password <PASSWORD> --network <NAME>
4825
+ get-my-wallet-meta --wallet <NAME> --password <PASSWORD> --network <NAME>
4664
4826
  Check whether a wallet matches the on-chain channel registration
4665
4827
 
4828
+ get-my-l1-address --private-key <HEX>
4829
+ Derive the L1 address for a private key
4830
+
4831
+ list-local-wallets [--network <NAME>] [--channel-name <NAME>]
4832
+ List saved local wallet names that can be reused with --wallet
4833
+
4666
4834
  deposit-channel --wallet <NAME> --password <PASSWORD> --network <NAME> --amount <TOKENS>
4667
4835
  Move bridged funds into the channel L2 accounting balance
4668
4836
 
@@ -4927,6 +5095,7 @@ function writePrivateStateCliInstallManifest({
4927
5095
  includeLocalArtifacts,
4928
5096
  localDeploymentBaseRoot,
4929
5097
  deploymentArtifacts,
5098
+ groth16Runtime,
4930
5099
  }) {
4931
5100
  const manifestPath = privateStateCliInstallManifestPath(deploymentArtifacts.cacheBaseRoot);
4932
5101
  const manifest = {
@@ -4941,6 +5110,7 @@ function writePrivateStateCliInstallManifest({
4941
5110
  includeLocalArtifacts,
4942
5111
  localDeploymentBaseRoot,
4943
5112
  artifactCacheRoot: deploymentArtifacts.cacheBaseRoot,
5113
+ groth16Runtime,
4944
5114
  installedDeploymentArtifacts: deploymentArtifacts.installed.map((entry) => ({
4945
5115
  chainId: entry.chainId,
4946
5116
  source: entry.source,
@@ -4966,6 +5136,8 @@ function buildDoctorReport() {
4966
5136
  const installManifest = readJsonIfExists(installManifestPath);
4967
5137
  const dependencyReports = collectDependencyPackageReports(installManifest);
4968
5138
  const tokamakCli = inspectTokamakCliRuntime();
5139
+ const groth16Runtime = inspectGroth16Runtime();
5140
+ const gpuDockerReadiness = inspectGpuDockerReadiness(tokamakCli);
4969
5141
  const checks = [
4970
5142
  {
4971
5143
  name: "dependency package versions",
@@ -4993,6 +5165,28 @@ function buildDoctorReport() {
4993
5165
  })),
4994
5166
  },
4995
5167
  },
5168
+ {
5169
+ name: "tokamak docker gpu readiness",
5170
+ ok: gpuDockerReadiness.ok,
5171
+ details: {
5172
+ expectedUseGpus: gpuDockerReadiness.expectedUseGpus,
5173
+ liveUseGpus: gpuDockerReadiness.liveUseGpus,
5174
+ mismatch: gpuDockerReadiness.mismatch,
5175
+ mismatchError: gpuDockerReadiness.mismatchError,
5176
+ hostNvidiaSmi: summarizeProbeResult(gpuDockerReadiness.hostNvidiaSmi),
5177
+ dockerNvidiaSmi: summarizeProbeResult(gpuDockerReadiness.dockerNvidiaSmi),
5178
+ },
5179
+ },
5180
+ {
5181
+ name: "groth16 runtime",
5182
+ ok: groth16Runtime.installed,
5183
+ details: {
5184
+ packageRoot: groth16Runtime.packageRoot,
5185
+ workspaceRoot: groth16Runtime.workspaceRoot,
5186
+ doctorStatus: groth16Runtime.doctor.status,
5187
+ checks: groth16Runtime.checks,
5188
+ },
5189
+ },
4996
5190
  ];
4997
5191
 
4998
5192
  return {
@@ -5012,10 +5206,25 @@ function buildDoctorReport() {
5012
5206
  },
5013
5207
  dependencies: dependencyReports,
5014
5208
  tokamakCli,
5209
+ groth16Runtime,
5210
+ gpuDockerReadiness,
5015
5211
  checks,
5016
5212
  };
5017
5213
  }
5018
5214
 
5215
+ function installGroth16RuntimeForPrivateState({ docker }) {
5216
+ const packageRoot = resolveGroth16PackageRoot();
5217
+ const entryPath = resolveGroth16CliEntryPath(packageRoot);
5218
+ const args = [entryPath, "--install"];
5219
+ if (docker) {
5220
+ args.push("--docker");
5221
+ }
5222
+ run(process.execPath, args, { cwd: packageRoot });
5223
+ const runtime = inspectGroth16Runtime();
5224
+ expect(runtime.installed, "Groth16 runtime install completed, but tokamak-groth16 --doctor still reports an unhealthy runtime.");
5225
+ return runtime;
5226
+ }
5227
+
5019
5228
  function collectDependencyPackageReports(installManifest = null) {
5020
5229
  const installVersions = new Map(
5021
5230
  Array.isArray(installManifest?.dependencies)
@@ -5087,6 +5296,35 @@ function findPackageJsonForName(startDir, expectedName) {
5087
5296
  throw new Error(`Cannot locate package.json for ${expectedName} above ${startDir}.`);
5088
5297
  }
5089
5298
 
5299
+ function resolveGroth16PackageRoot() {
5300
+ const publicDriveCrsPath = require.resolve("@tokamak-private-dapps/groth16/public-drive-crs");
5301
+ return path.dirname(findPackageJsonForName(path.dirname(publicDriveCrsPath), "@tokamak-private-dapps/groth16"));
5302
+ }
5303
+
5304
+ function resolveGroth16CliEntryPath(packageRoot = resolveGroth16PackageRoot()) {
5305
+ return path.join(packageRoot, "cli", "tokamak-groth16-cli.mjs");
5306
+ }
5307
+
5308
+ function inspectGroth16Runtime() {
5309
+ const packageRoot = resolveGroth16PackageRoot();
5310
+ const entryPath = resolveGroth16CliEntryPath(packageRoot);
5311
+ const doctor = runCaptured(process.execPath, [entryPath, "--doctor", "--verbose"], { cwd: packageRoot });
5312
+ const stdout = stripAnsi(doctor.stdout).trim();
5313
+ const stderr = stripAnsi(doctor.stderr).trim();
5314
+ const report = parseJsonReport(stdout);
5315
+ return {
5316
+ installed: doctor.status === 0 && report?.ok === true,
5317
+ packageRoot,
5318
+ workspaceRoot: report?.workspaceRoot ?? null,
5319
+ checks: report?.checks ?? [],
5320
+ doctor: {
5321
+ status: doctor.status,
5322
+ stdout,
5323
+ stderr,
5324
+ },
5325
+ };
5326
+ }
5327
+
5090
5328
  function inspectTokamakCliRuntime() {
5091
5329
  const doctor = runCaptured(tokamakCliCommand, [...tokamakCliBaseArgs, "--doctor"], {
5092
5330
  cwd: resolveTokamakCliPackageRoot(),
@@ -5118,6 +5356,84 @@ function inspectTokamakCliRuntime() {
5118
5356
  };
5119
5357
  }
5120
5358
 
5359
+ function inspectGpuDockerReadiness(tokamakCli) {
5360
+ const hostNvidiaSmi = runProbe("nvidia-smi", ["--query-gpu=name,driver_version", "--format=csv,noheader"]);
5361
+ const dockerNvidiaSmi = runProbe("docker", [
5362
+ "run",
5363
+ "--rm",
5364
+ "--gpus",
5365
+ "all",
5366
+ DOCKER_CUDA_PROBE_IMAGE,
5367
+ "nvidia-smi",
5368
+ ]);
5369
+ const expectedUseGpus = Boolean(tokamakCli.cudaCompatible);
5370
+ const liveUseGpus = hostNvidiaSmi.ok && dockerNvidiaSmi.ok;
5371
+ const mismatch = expectedUseGpus !== liveUseGpus;
5372
+ return {
5373
+ ok: !mismatch,
5374
+ expectedUseGpus,
5375
+ liveUseGpus,
5376
+ mismatch,
5377
+ mismatchError: mismatch
5378
+ ? [
5379
+ "Tokamak CLI Docker GPU metadata does not match live NVIDIA/Docker GPU probes.",
5380
+ `metadata useGpus=${expectedUseGpus}; live useGpus=${liveUseGpus}.`,
5381
+ ].join(" ")
5382
+ : null,
5383
+ probeImage: DOCKER_CUDA_PROBE_IMAGE,
5384
+ hostNvidiaSmi,
5385
+ dockerNvidiaSmi,
5386
+ };
5387
+ }
5388
+
5389
+ function runProbe(command, args) {
5390
+ const result = spawnSync(command, args, {
5391
+ encoding: "utf8",
5392
+ timeout: DOCTOR_GPU_PROBE_TIMEOUT_MS,
5393
+ stdio: ["ignore", "pipe", "pipe"],
5394
+ });
5395
+ return {
5396
+ command,
5397
+ args,
5398
+ ok: !result.error && result.status === 0,
5399
+ status: result.status,
5400
+ signal: result.signal,
5401
+ error: result.error ? result.error.message : null,
5402
+ stdout: stripAnsi(result.stdout ?? "").trim(),
5403
+ stderr: stripAnsi(result.stderr ?? "").trim(),
5404
+ timedOut: result.error?.code === "ETIMEDOUT",
5405
+ };
5406
+ }
5407
+
5408
+ function summarizeProbeResult(result) {
5409
+ return {
5410
+ command: [result.command, ...result.args].join(" "),
5411
+ ok: result.ok,
5412
+ status: result.status,
5413
+ signal: result.signal,
5414
+ error: result.error,
5415
+ timedOut: result.timedOut,
5416
+ stdout: truncateText(result.stdout, 2000),
5417
+ stderr: truncateText(result.stderr, 2000),
5418
+ };
5419
+ }
5420
+
5421
+ function truncateText(value, maxLength) {
5422
+ const text = String(value ?? "");
5423
+ if (text.length <= maxLength) {
5424
+ return text;
5425
+ }
5426
+ return `${text.slice(0, maxLength)}...`;
5427
+ }
5428
+
5429
+ function parseJsonReport(value) {
5430
+ try {
5431
+ return JSON.parse(value);
5432
+ } catch {
5433
+ return null;
5434
+ }
5435
+ }
5436
+
5121
5437
  function resolveTokamakCliCacheRoot() {
5122
5438
  return path.resolve(process.env.TOKAMAK_ZKEVM_CLI_CACHE_DIR ?? path.join(os.homedir(), ".tokamak-zk-evm"));
5123
5439
  }