@tokamak-private-dapps/private-state-cli 2.4.2 → 2.4.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
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 2.4.3 - 2026-06-01
6
+
7
+ - Added pre-submit transaction dry-runs for every transaction-sending CLI command.
8
+ - Added post-proof local prechecks before dry-run for proof-backed commands.
9
+ - Improved dry-run and submit failure messages with decoded revert details when available.
10
+ - Standardized CLI `--json` output so final success and failure results are JSON objects on stdout.
11
+ - Changed progress, warning, and informational events to emit JSON Lines on stderr in `--json` mode.
12
+ - Added structured JSON command-reference output for `help commands --json` and `--help --json`.
13
+
5
14
  ## 2.4.2 - 2026-05-30
6
15
 
7
16
  - Changed channel workspace recovery guidance so CLI help, guide output, agent instructions, and documentation direct
package/README.md CHANGED
@@ -170,7 +170,8 @@ A common note-use flow after channel policy review is:
170
170
  `channel join` pays any join toll directly from the L1 wallet; `account deposit-bridge` funds later channel liquidity and does not pay the join toll.
171
171
 
172
172
  Use `private-state-cli help commands` for the full command list and required options. `private-state-cli --help`
173
- continues to print the same command list for shell compatibility.
173
+ continues to print the same command list for shell compatibility. Add `--json` to either form to print the command
174
+ reference as structured JSON on stdout.
174
175
 
175
176
  ### Action-impact acknowledgement
176
177
 
@@ -524,6 +525,15 @@ LLM agents that guide users through this CLI should read [`agents.md`](agents.md
524
525
  commands. That file contains the agent-specific operating rules, including secret-handling boundaries, onboarding
525
526
  sequence, acknowledgement handling, recovery behavior, and error-response policy.
526
527
 
528
+ When `--json` is used, the CLI follows one output contract for all commands:
529
+
530
+ - the final success result is one JSON object on stdout
531
+ - command failures are one JSON object on stdout with `ok: false`
532
+ - progress, warning, and informational events are JSON Lines on stderr
533
+ - human-readable mode remains the default when `--json` is omitted
534
+
535
+ Agents should parse stdout for the final result and may stream stderr JSONL events to explain progress to the user.
536
+
527
537
  ## Artifacts
528
538
 
529
539
  Proof-backed and channel-mutating commands require full installed bridge, DApp, and Groth16 artifacts. Run
package/agents.md CHANGED
@@ -48,6 +48,9 @@ activity or as a bridge-wide disclosure rule for every DApp.
48
48
  `private-state-cli help transaction-fees --network <NETWORK> --json` and answer from the returned `rows`. If the
49
49
  network is unclear, ask which network to use. Do not tell the user to ask the developer unless the command fails after
50
50
  following the CLI's printed corrective guidance.
51
+ - Prefer `--json` when running commands on behalf of a user. In JSON mode, parse stdout as the final success or failure
52
+ result. Failures use `ok: false` on stdout. Progress, warning, and informational events are emitted as JSON Lines on
53
+ stderr; stream or summarize those events for the human user instead of treating them as fatal by default.
51
54
  - When `channel recover-workspace` or `wallet recover-workspace` is unexpectedly slow, first inspect the RPC provider
52
55
  configured by `set rpc`. Explain that recovery speed is dominated by `eth_getLogs` block range cap and log request
53
56
  rate. Suggest re-running `set rpc` with a provider that supports a larger block range cap, such as Ankr or Chainnodes
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  assertHelpCommandsArgs,
3
3
  assertVersionArgs,
4
+ cliOutput,
4
5
  configureOutput,
5
- formatCliErrorForDisplay,
6
6
  parseArgs,
7
7
  printHelp,
8
8
  printVersion,
@@ -52,11 +52,7 @@ export async function runPrivateStateCli(argv) {
52
52
  }
53
53
  await command(args);
54
54
  } catch (error) {
55
- console.error(formatCliErrorForDisplay(error, args));
55
+ cliOutput.error(error, args);
56
56
  process.exitCode = 1;
57
57
  }
58
58
  }
59
-
60
- export function privateStateCliDispatchTable() {
61
- return COMMANDS;
62
- }
@@ -313,8 +313,11 @@ export const PRIVATE_STATE_CLI_COMMANDS = Object.freeze([
313
313
  id: "help-commands",
314
314
  display: "help commands",
315
315
  description: "Show the private-state CLI command reference.",
316
- fields: [],
317
- usage: "no options",
316
+ fields: ["json"],
317
+ usage: "optional --json",
318
+ help: [
319
+ "Use --json to emit the full command reference as structured JSON on stdout.",
320
+ ],
318
321
  },
319
322
  {
320
323
  id: "help-update",
package/lib/runtime.mjs CHANGED
@@ -166,6 +166,8 @@ const CLI_ERROR_CODES = Object.freeze({
166
166
  MISSING_CHANNEL_REGISTRATION: "MISSING_CHANNEL_REGISTRATION",
167
167
  STALE_WORKSPACE: "STALE_WORKSPACE",
168
168
  STALE_CHANNEL_ROOT: "STALE_CHANNEL_ROOT",
169
+ TX_DRY_RUN_FAILED: "TX_DRY_RUN_FAILED",
170
+ TX_SUBMIT_FAILED: "TX_SUBMIT_FAILED",
169
171
  });
170
172
 
171
173
  class PrivateStateCliError extends Error {
@@ -288,7 +290,13 @@ function printImmutableChannelPolicyWarning({
288
290
  "Do not sign if any snapshot value is unexpected or has not been reviewed.",
289
291
  );
290
292
  }
291
- console.error(details.join("\n"));
293
+ cliOutput.warning("channel-policy", details.join("\n"), {
294
+ action,
295
+ channelName,
296
+ channelId: channelId.toString(),
297
+ channelManager,
298
+ policySnapshot,
299
+ });
292
300
  }
293
301
 
294
302
  const ACTION_IMPACT_SUMMARIES = Object.freeze({
@@ -483,7 +491,16 @@ function printActionImpactSummary(summary, details) {
483
491
  lines.push(`- Exchange-controlled address warning: ${summary.exchangeControlledAddressWarning}`);
484
492
  }
485
493
  lines.push(`- Confirmation: pass --acknowledge-action-impact or type the exact confirmation phrase when prompted.`);
486
- console.error(lines.join("\n"));
494
+ cliOutput.warning("action-impact", lines.join("\n"), {
495
+ command: summary.display,
496
+ l1PublicEvent: summary.l1PublicEvent,
497
+ privateNoteState: summary.privateNoteState,
498
+ publicFields: normalizeImpactLines(summary.publicFields, details),
499
+ notPublic: normalizeImpactLines(summary.notPublic, details),
500
+ noteProvenance: summary.noteProvenance,
501
+ policy: summary.policy,
502
+ exchangeControlledAddressWarning: summary.exchangeControlledAddressWarning ?? null,
503
+ });
487
504
  }
488
505
 
489
506
  function normalizeImpactLines(value, details) {
@@ -609,8 +626,15 @@ async function handleChannelCreate({ args, network, provider }) {
609
626
  channelId,
610
627
  policySnapshot,
611
628
  });
612
- const receipt =
613
- await waitForReceipt(await bridgeCore.createChannel(channelId, dappId, joinToll, dapp.metadataDigest));
629
+ const receipt = await dryRunThenSubmitTransaction({
630
+ operationName: "channel create",
631
+ call: contractTxCall(
632
+ bridgeCore.createChannel,
633
+ [channelId, dappId, joinToll, dapp.metadataDigest],
634
+ undefined,
635
+ bridgeCore.interface,
636
+ ),
637
+ });
614
638
  const channelInfo = await bridgeCore.getChannel(channelId);
615
639
 
616
640
  const workspaceResult = await syncChannelWorkspace({
@@ -625,7 +649,7 @@ async function handleChannelCreate({ args, network, provider }) {
625
649
  progressAction: "channel create",
626
650
  });
627
651
 
628
- printJson({
652
+ cliOutput.result({
629
653
  action: "channel create",
630
654
  channelName,
631
655
  channelId: channelId.toString(),
@@ -767,7 +791,7 @@ async function handleWorkspaceInit({ args, network, provider }) {
767
791
  })
768
792
  : null;
769
793
 
770
- printJson({
794
+ cliOutput.result({
771
795
  action: "channel recover-workspace",
772
796
  source: workspace.recoverySource ?? recoverySource,
773
797
  workspace: workspaceName,
@@ -804,7 +828,7 @@ async function handleGetChannel({ args, network, provider }) {
804
828
  channelInfo = await bridgeCore.getChannel(channelId);
805
829
  } catch (error) {
806
830
  if (isContractError(error, bridgeCore.interface, "UnknownChannel")) {
807
- printJson({
831
+ cliOutput.result({
808
832
  action: "channel get-meta",
809
833
  channelName,
810
834
  channelId: channelId.toString(),
@@ -843,7 +867,7 @@ async function handleGetChannel({ args, network, provider }) {
843
867
  readChannelRefundSchedule(channelManager),
844
868
  ]);
845
869
 
846
- printJson({
870
+ cliOutput.result({
847
871
  action: "channel get-meta",
848
872
  channelName,
849
873
  channelId: channelId.toString(),
@@ -880,10 +904,18 @@ async function handleSetChannelWorkspaceMirror({ args, network, provider }) {
880
904
  );
881
905
  const channelId = deriveChannelIdFromName(channelName);
882
906
  const previousUrl = await readChannelWorkspaceMirror({ bridgeCore, channelId });
883
- const receipt = await waitForReceipt(await bridgeCore.setChannelWorkspaceMirror(channelId, url));
907
+ const receipt = await dryRunThenSubmitTransaction({
908
+ operationName: "channel set-workspace-mirror",
909
+ call: contractTxCall(
910
+ bridgeCore.setChannelWorkspaceMirror,
911
+ [channelId, url],
912
+ undefined,
913
+ bridgeCore.interface,
914
+ ),
915
+ });
884
916
  const currentUrl = await readChannelWorkspaceMirror({ bridgeCore, channelId });
885
917
 
886
- printJson({
918
+ cliOutput.result({
887
919
  action: "channel set-workspace-mirror",
888
920
  channelName,
889
921
  channelId: channelId.toString(),
@@ -2210,12 +2242,28 @@ async function handleDepositBridge({ args, network, provider }) {
2210
2242
  signer,
2211
2243
  );
2212
2244
  let nextNonce = await provider.getTransactionCount(signer.address, "pending");
2213
- const approveReceipt =
2214
- await waitForReceipt(await asset.approve(bridgeVaultContext.bridgeTokenVaultAddress, amount, { nonce: nextNonce++ }));
2215
- const fundReceipt = await waitForReceipt(await bridgeTokenVault.fund(amount, { nonce: nextNonce++ }));
2245
+ const approveReceipt = await dryRunThenSubmitTransaction({
2246
+ operationName: "account deposit-bridge approve",
2247
+ call: contractTxCall(
2248
+ asset.approve,
2249
+ [bridgeVaultContext.bridgeTokenVaultAddress, amount],
2250
+ { nonce: nextNonce++ },
2251
+ asset.interface,
2252
+ ),
2253
+ });
2254
+ const fundReceipt = await dryRunThenSubmitTransaction({
2255
+ operationName: "account deposit-bridge fund",
2256
+ call: contractTxCall(
2257
+ bridgeTokenVault.fund,
2258
+ [amount],
2259
+ { nonce: nextNonce++ },
2260
+ bridgeTokenVault.interface,
2261
+ ),
2262
+ submittedBefore: [submittedReceiptSummary("account deposit-bridge approve", approveReceipt)],
2263
+ });
2216
2264
  const availableBalance = await bridgeTokenVault.availableBalanceOf(signer.address);
2217
2265
 
2218
- printJson({
2266
+ cliOutput.result({
2219
2267
  action: "account deposit-bridge",
2220
2268
  amountInput,
2221
2269
  amountBaseUnits: amount.toString(),
@@ -2243,7 +2291,7 @@ async function handleAccountGetBridgeFund({ args, provider }) {
2243
2291
  );
2244
2292
  const availableBalance = await bridgeTokenVault.availableBalanceOf(signer.address);
2245
2293
 
2246
- printJson({
2294
+ cliOutput.result({
2247
2295
  action: "account get-bridge-fund",
2248
2296
  l1Address: signer.address,
2249
2297
  bridgeTokenVault: bridgeVaultContext.bridgeTokenVaultAddress,
@@ -2393,7 +2441,7 @@ async function handleRecoverWallet({ args, network, provider, rpcUrl }) {
2393
2441
  latestBlock: recoveryEventScan.scanRange.toBlock,
2394
2442
  });
2395
2443
 
2396
- printJson({
2444
+ cliOutput.result({
2397
2445
  action: "wallet recover-workspace",
2398
2446
  status,
2399
2447
  wallet: walletName,
@@ -2491,7 +2539,7 @@ async function handleInstallZkEvm({ args }) {
2491
2539
  tokamakCliRuntime,
2492
2540
  groth16Runtime,
2493
2541
  });
2494
- printJson({
2542
+ cliOutput.result({
2495
2543
  action: "install",
2496
2544
  installMode,
2497
2545
  selectedVersions,
@@ -2532,7 +2580,7 @@ async function handleUninstall() {
2532
2580
  });
2533
2581
  const globalPackage = uninstallGlobalPrivateStateCliPackage();
2534
2582
 
2535
- printJson({
2583
+ cliOutput.result({
2536
2584
  action: "uninstall",
2537
2585
  confirmationAccepted: true,
2538
2586
  removedPrivateStateRoots,
@@ -2559,7 +2607,7 @@ async function handleSetRpc({ args }) {
2559
2607
  LOG_CHUNK_SIZE: rpcScanLimits.blockRangeCap,
2560
2608
  RPC_BLOCK_RANGE_CAP: rpcScanLimits.blockRangeCap,
2561
2609
  });
2562
- printJson({
2610
+ cliOutput.result({
2563
2611
  action: "set rpc",
2564
2612
  network: networkName,
2565
2613
  rpcConfigPath: rpcConfigEnvPath(networkName),
@@ -2964,12 +3012,12 @@ async function handleUpdate() {
2964
3012
  };
2965
3013
 
2966
3014
  if (!updateAvailable) {
2967
- printJson(result);
3015
+ cliOutput.result(result);
2968
3016
  return;
2969
3017
  }
2970
3018
 
2971
3019
  if (runningFromRepositoryCheckout) {
2972
- printJson({
3020
+ cliOutput.result({
2973
3021
  ...result,
2974
3022
  reason: "running from a repository checkout; update the checkout with git/npm instead of mutating source files",
2975
3023
  });
@@ -2977,7 +3025,7 @@ async function handleUpdate() {
2977
3025
  }
2978
3026
 
2979
3027
  if (!globalPackage.installed) {
2980
- printJson({
3028
+ cliOutput.result({
2981
3029
  ...result,
2982
3030
  reason: "global npm package is not installed; install or update the CLI with the printed command",
2983
3031
  });
@@ -2991,7 +3039,7 @@ async function handleUpdate() {
2991
3039
  stripAnsi(install.stderr || install.stdout).trim(),
2992
3040
  ].filter(Boolean).join(" "));
2993
3041
  }
2994
- printJson({
3042
+ cliOutput.result({
2995
3043
  ...result,
2996
3044
  attempted: true,
2997
3045
  updated: true,
@@ -3043,18 +3091,14 @@ function isRepositoryCliPackageRoot(packageRoot) {
3043
3091
 
3044
3092
  async function handleDoctor({ args }) {
3045
3093
  const report = buildDoctorReport({ probeGpu: args.gpu === true });
3046
- if (isJsonOutputRequested()) {
3047
- printJson(report);
3048
- } else {
3049
- printDoctorHumanReport(report);
3050
- }
3094
+ cliOutput.result(report);
3051
3095
  if (!report.ok) {
3052
3096
  process.exitCode = 1;
3053
3097
  }
3054
3098
  }
3055
3099
 
3056
3100
  function handleObserver() {
3057
- printJson({
3101
+ cliOutput.result({
3058
3102
  action: "observer",
3059
3103
  url: PRIVATE_STATE_OBSERVER_URL,
3060
3104
  scope: "Public monitoring observer for Tokamak Private App Channels and the private-state DApp.",
@@ -3069,7 +3113,7 @@ function handleInvestigator() {
3069
3113
  const htmlPath = resolveInvestigatorIndexPath();
3070
3114
  const fileUrl = pathToFileURL(htmlPath).href;
3071
3115
  const browser = openFileInDefaultBrowser(fileUrl);
3072
- printJson({
3116
+ cliOutput.result({
3073
3117
  action: "investigator",
3074
3118
  htmlPath,
3075
3119
  fileUrl,
@@ -3138,7 +3182,7 @@ async function handleTransactionFees({ network, provider, rpcUrl }) {
3138
3182
  ethUsd,
3139
3183
  });
3140
3184
 
3141
- printJson({
3185
+ cliOutput.result({
3142
3186
  action: "transaction-fees",
3143
3187
  generatedAt: new Date().toISOString(),
3144
3188
  network: network.name,
@@ -3266,7 +3310,7 @@ function trimFixedNumber(value, maxDecimals) {
3266
3310
 
3267
3311
  function handleAccountGetL1Address({ args }) {
3268
3312
  const signer = requireL1Signer(args);
3269
- printJson({
3313
+ cliOutput.result({
3270
3314
  action: "account get-l1-address",
3271
3315
  l1Address: signer.address,
3272
3316
  account: args.account ?? null,
@@ -3293,7 +3337,7 @@ function handleAccountImport({ args }) {
3293
3337
  l1Address: getAddress(signer.address),
3294
3338
  privateKeyPath,
3295
3339
  }, 0o600);
3296
- printJson({
3340
+ cliOutput.result({
3297
3341
  action: "account import",
3298
3342
  account,
3299
3343
  network: networkName,
@@ -3314,7 +3358,7 @@ function handleListLocalWallets({ args }) {
3314
3358
  channelFilter,
3315
3359
  });
3316
3360
 
3317
- printJson({
3361
+ cliOutput.result({
3318
3362
  action: "wallet list",
3319
3363
  workspaceRoot,
3320
3364
  filters: {
@@ -3382,7 +3426,7 @@ function handleWalletExportBackup({ args }) {
3382
3426
  archive.writeZip(outputPath);
3383
3427
  protectSecretFile(outputPath, "wallet export ZIP");
3384
3428
 
3385
- printJson({
3429
+ cliOutput.result({
3386
3430
  action: "wallet export backup",
3387
3431
  output: outputPath,
3388
3432
  exportMode: manifest.exportMode,
@@ -3407,7 +3451,7 @@ function handleWalletExportKey({ args, keyKind }) {
3407
3451
  validateWalletKeyPayload(payload, keyKind);
3408
3452
  fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, { mode: 0o600 });
3409
3453
  protectSecretFile(outputPath, `${keyKind} key export`);
3410
- printJson({
3454
+ cliOutput.result({
3411
3455
  action: `wallet export ${keyKind}-key`,
3412
3456
  wallet: wallet.walletName,
3413
3457
  network: networkName,
@@ -3452,7 +3496,7 @@ function handleWalletImportBackup({ args }) {
3452
3496
 
3453
3497
  commitWalletImportFiles({ targetRoot, plannedWrites });
3454
3498
 
3455
- printJson({
3499
+ cliOutput.result({
3456
3500
  action: "wallet import backup",
3457
3501
  input: inputPath,
3458
3502
  exportMode: manifest.exportMode,
@@ -3495,7 +3539,7 @@ function handleWalletImportKey({ args, keyKind }) {
3495
3539
  writeJson(metadataPath, metadata);
3496
3540
  }
3497
3541
  }
3498
- printJson({
3542
+ cliOutput.result({
3499
3543
  action: `wallet import ${keyKind}-key`,
3500
3544
  input: inputPath,
3501
3545
  network: networkName,
@@ -3637,7 +3681,7 @@ async function handleGuide({ args }) {
3637
3681
  "help guide --network anvil",
3638
3682
  ],
3639
3683
  });
3640
- printJson(guide);
3684
+ cliOutput.result(guide);
3641
3685
  return;
3642
3686
  }
3643
3687
 
@@ -3650,7 +3694,7 @@ async function handleGuide({ args }) {
3650
3694
  command: "help guide --network <NAME>",
3651
3695
  why: `The requested network ${networkName} is not supported by the CLI network config.`,
3652
3696
  });
3653
- printJson(guide);
3697
+ cliOutput.result(guide);
3654
3698
  return;
3655
3699
  }
3656
3700
 
@@ -3737,7 +3781,7 @@ async function handleGuide({ args }) {
3737
3781
 
3738
3782
  destroyGuideProvider(provider);
3739
3783
  applyGuideNextAction(guide);
3740
- printJson(guide);
3784
+ cliOutput.result(guide);
3741
3785
  }
3742
3786
 
3743
3787
  function inspectGuideLocalState(args) {
@@ -4226,7 +4270,7 @@ async function handleWalletGetMeta({ args, provider }) {
4226
4270
  provider,
4227
4271
  });
4228
4272
 
4229
- printJson({
4273
+ cliOutput.result({
4230
4274
  action: "wallet get-meta",
4231
4275
  wallet: wallet.walletName,
4232
4276
  ...walletLifecycleMetadata(wallet.wallet),
@@ -4560,7 +4604,7 @@ async function handleWalletGetChannelFund({ args, provider }) {
4560
4604
  provider,
4561
4605
  });
4562
4606
 
4563
- printJson({
4607
+ cliOutput.result({
4564
4608
  action: "wallet get-channel-fund",
4565
4609
  wallet: wallet.walletName,
4566
4610
  network: walletMetadata.network,
@@ -4641,20 +4685,32 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
4641
4685
  channelId: context.workspace.channelId,
4642
4686
  });
4643
4687
  if (joinToll !== 0n) {
4644
- approveReceipt = await waitForReceipt(
4645
- await asset.approve(context.workspace.bridgeTokenVault, joinToll, { nonce: nextNonce++ }),
4646
- );
4688
+ approveReceipt = await dryRunThenSubmitTransaction({
4689
+ operationName: "channel join approve",
4690
+ call: contractTxCall(
4691
+ asset.approve,
4692
+ [context.workspace.bridgeTokenVault, joinToll],
4693
+ { nonce: nextNonce++ },
4694
+ asset.interface,
4695
+ ),
4696
+ });
4647
4697
  }
4648
- receipt = await waitForReceipt(
4649
- await context.bridgeTokenVault.connect(signer).joinChannel(
4650
- ethers.toBigInt(context.workspace.channelId),
4651
- l2Identity.l2Address,
4652
- storageKey,
4653
- leafIndex,
4654
- noteReceiveKeyMaterial.noteReceivePubKey,
4698
+ receipt = await dryRunThenSubmitTransaction({
4699
+ operationName: "channel join",
4700
+ call: contractTxCall(
4701
+ context.bridgeTokenVault.connect(signer).joinChannel,
4702
+ [
4703
+ ethers.toBigInt(context.workspace.channelId),
4704
+ l2Identity.l2Address,
4705
+ storageKey,
4706
+ leafIndex,
4707
+ noteReceiveKeyMaterial.noteReceivePubKey,
4708
+ ],
4655
4709
  { nonce: nextNonce++ },
4710
+ context.bridgeTokenVault.interface,
4656
4711
  ),
4657
- );
4712
+ submittedBefore: approveReceipt ? [submittedReceiptSummary("channel join approve", approveReceipt)] : [],
4713
+ });
4658
4714
  const registered = await context.channelManager.getChannelTokenVaultRegistration(signer.address);
4659
4715
  const lifecycleEpoch = await walletEpochFromJoinReceipt({
4660
4716
  receipt,
@@ -4683,7 +4739,7 @@ async function handleJoinChannel({ args, network, provider, rpcUrl }) {
4683
4739
  rpcUrl,
4684
4740
  });
4685
4741
 
4686
- printJson({
4742
+ cliOutput.result({
4687
4743
  action: "channel join",
4688
4744
  workspace: context.workspaceName,
4689
4745
  wallet: walletContext.walletName,
@@ -4733,16 +4789,22 @@ async function handleExitChannel({ args, provider }) {
4733
4789
  ].join(" "),
4734
4790
  );
4735
4791
  const [refundAmount, refundBps] = await context.channelManager.getExitTollRefundQuote(ownerSigner.address);
4736
- const receipt = await waitForReceipt(
4737
- await context.bridgeTokenVault.connect(ownerSigner).exitChannel(ethers.toBigInt(context.workspace.channelId)),
4738
- );
4792
+ const receipt = await dryRunThenSubmitTransaction({
4793
+ operationName: "channel exit",
4794
+ call: contractTxCall(
4795
+ context.bridgeTokenVault.connect(ownerSigner).exitChannel,
4796
+ [ethers.toBigInt(context.workspace.channelId)],
4797
+ undefined,
4798
+ context.bridgeTokenVault.interface,
4799
+ ),
4800
+ });
4739
4801
  const lifecycleEpoch = await markWalletEpochExited({
4740
4802
  walletContext,
4741
4803
  receipt,
4742
4804
  provider,
4743
4805
  });
4744
4806
 
4745
- printJson({
4807
+ cliOutput.result({
4746
4808
  action: "channel exit",
4747
4809
  wallet: walletContext.walletName,
4748
4810
  network: walletMetadata.network,
@@ -4867,16 +4929,22 @@ async function handleGrothVaultMove({ args, provider, direction }) {
4867
4929
  });
4868
4930
 
4869
4931
  const methodName = direction === "deposit" ? "depositToChannelVault" : "withdrawFromChannelVault";
4870
- await assertWorkspaceAlignedWithChain(context);
4871
4932
  emitProgress(operationName, "submitting");
4872
- const receipt = await submitProofBackedRootUpdate({
4933
+ const receipt = await dryRunThenSubmitTransaction({
4934
+ operationName,
4873
4935
  context,
4874
4936
  walletName: walletContext.walletName,
4875
- operationName,
4876
- submit: () => bridgeTokenVault[methodName](
4877
- ethers.toBigInt(context.workspace.channelId),
4878
- transition.proof,
4879
- transition.update,
4937
+ operationDir,
4938
+ precheck: () => precheckGrothRootUpdate({ context, transition, operationName }),
4939
+ call: contractTxCall(
4940
+ bridgeTokenVault[methodName],
4941
+ [
4942
+ ethers.toBigInt(context.workspace.channelId),
4943
+ transition.proof,
4944
+ transition.update,
4945
+ ],
4946
+ undefined,
4947
+ bridgeTokenVault.interface,
4880
4948
  ),
4881
4949
  });
4882
4950
  const onchainRootVectorHash = normalizeBytes32Hex(await context.channelManager.currentRootVectorHash());
@@ -4900,7 +4968,7 @@ async function handleGrothVaultMove({ args, provider, direction }) {
4900
4968
  });
4901
4969
 
4902
4970
  emitProgress(operationName, "done");
4903
- printJson({
4971
+ cliOutput.result({
4904
4972
  action: operationName,
4905
4973
  workspace: context.workspaceName,
4906
4974
  wallet: walletContext.walletName,
@@ -4934,9 +5002,17 @@ async function handleWithdrawBridge({ args, network, provider }) {
4934
5002
  bridgeVaultContext.bridgeAbiManifest.contracts.bridgeTokenVault.abi,
4935
5003
  signer,
4936
5004
  );
4937
- const receipt = await waitForReceipt(await bridgeTokenVault.claimToWallet(amount));
5005
+ const receipt = await dryRunThenSubmitTransaction({
5006
+ operationName: "account withdraw-bridge",
5007
+ call: contractTxCall(
5008
+ bridgeTokenVault.claimToWallet,
5009
+ [amount],
5010
+ undefined,
5011
+ bridgeTokenVault.interface,
5012
+ ),
5013
+ });
4938
5014
 
4939
- printJson({
5015
+ cliOutput.result({
4940
5016
  action: "account withdraw-bridge",
4941
5017
  l1Address: signer.address,
4942
5018
  amountInput,
@@ -5067,7 +5143,7 @@ async function handleMintNotes({ args, provider }) {
5067
5143
  preparedContextResult,
5068
5144
  });
5069
5145
 
5070
- printJson({
5146
+ cliOutput.result({
5071
5147
  action: "wallet mint-notes",
5072
5148
  wallet: wallet.walletName,
5073
5149
  workspace: execution.context.workspaceName,
@@ -5142,7 +5218,7 @@ async function handleRedeemNotes({ args, provider }) {
5142
5218
  preparedContextResult,
5143
5219
  });
5144
5220
 
5145
- printJson({
5221
+ cliOutput.result({
5146
5222
  action: "wallet redeem-notes",
5147
5223
  wallet: wallet.walletName,
5148
5224
  workspace: execution.context.workspaceName,
@@ -5234,7 +5310,7 @@ async function handleWalletGetNotes({ args, provider }) {
5234
5310
  })
5235
5311
  : null;
5236
5312
 
5237
- printJson({
5313
+ cliOutput.result({
5238
5314
  action: "wallet get-notes",
5239
5315
  wallet: wallet.walletName,
5240
5316
  network: walletMetadata.network,
@@ -6002,7 +6078,7 @@ async function handleTransferNotes({ args, provider }) {
6002
6078
  counterpartyDirection: "sent",
6003
6079
  });
6004
6080
 
6005
- printJson({
6081
+ cliOutput.result({
6006
6082
  action: "wallet transfer-notes",
6007
6083
  wallet: wallet.walletName,
6008
6084
  workspace: execution.context.workspaceName,
@@ -7656,7 +7732,7 @@ async function executeWalletTemplateSend({
7656
7732
  functionName,
7657
7733
  templatePayload,
7658
7734
  }) {
7659
- await assertWorkspaceAlignedWithChain(context, signer.provider);
7735
+ await assertWorkspaceAlignedWithChain(context);
7660
7736
  assertWalletMatchesChannelContext(wallet, l2Identity, context);
7661
7737
  await assertChannelProofBackendVersionCompatibility({ context, operationName });
7662
7738
 
@@ -7712,19 +7788,26 @@ async function executeWalletTemplateSend({
7712
7788
  expectedFunctionRoot: context.workspace.functionRoot ?? context.workspace.policySnapshot?.functionRoot,
7713
7789
  });
7714
7790
  const aPubBlockHash = hashTokamakPublicInputs(payload.aPubBlock);
7715
- expect(
7716
- ethers.toBigInt(normalizeBytes32Hex(aPubBlockHash))
7717
- === ethers.toBigInt(normalizeBytes32Hex(context.workspace.aPubBlockHash)),
7718
- "Generated Tokamak proof does not match the channel aPubBlockHash. Check the workspace block_info.json context.",
7719
- );
7720
7791
 
7721
- await assertWorkspaceAlignedWithChain(context);
7722
7792
  emitProgress(operationName, "submitting");
7723
- const receipt = await submitProofBackedRootUpdate({
7793
+ const receipt = await dryRunThenSubmitTransaction({
7794
+ operationName,
7724
7795
  context,
7725
7796
  walletName: wallet.walletName,
7726
- operationName,
7727
- submit: () => context.channelManager.connect(txSubmitter).executeChannelTransaction(payload, functionProof),
7797
+ operationDir,
7798
+ precheck: () => precheckTokamakExecution({
7799
+ context,
7800
+ payload,
7801
+ functionProof,
7802
+ aPubBlockHash,
7803
+ operationName,
7804
+ }),
7805
+ call: contractTxCall(
7806
+ context.channelManager.connect(txSubmitter).executeChannelTransaction,
7807
+ [payload, functionProof],
7808
+ undefined,
7809
+ context.channelManager.interface,
7810
+ ),
7728
7811
  });
7729
7812
  await waitForProviderBlockAtLeast(provider, receipt.blockNumber, { action: operationName });
7730
7813
 
@@ -8399,25 +8482,219 @@ function isUnexpectedCurrentRootVectorError(error, context) {
8399
8482
  return String(error?.message ?? error).includes("UnexpectedCurrentRootVector");
8400
8483
  }
8401
8484
 
8402
- async function submitProofBackedRootUpdate({
8403
- context,
8404
- walletName,
8485
+ function precheckGrothRootUpdate({ context, transition, operationName }) {
8486
+ expect(transition && typeof transition === "object", `${operationName} proof precheck failed: missing transition.`);
8487
+ expect(transition.proof && typeof transition.proof === "object", `${operationName} proof precheck failed: missing Groth16 proof.`);
8488
+ expect(transition.update && typeof transition.update === "object", `${operationName} proof precheck failed: missing root update.`);
8489
+ expect(
8490
+ Array.isArray(transition.update.currentRootVector) && transition.update.currentRootVector.length > 0,
8491
+ `${operationName} proof precheck failed: missing current root vector.`,
8492
+ );
8493
+ expect(
8494
+ normalizeBytes32Hex(hashRootVector(transition.update.currentRootVector))
8495
+ === normalizeBytes32Hex(hashRootVector(context.currentSnapshot.stateRoots)),
8496
+ `${operationName} proof precheck failed: root update does not match the local workspace snapshot.`,
8497
+ );
8498
+ normalizeBytes32Hex(transition.update.updatedRoot);
8499
+ normalizeBytes32Hex(transition.update.currentUserKey);
8500
+ normalizeBytes32Hex(transition.update.updatedUserKey);
8501
+ expect(transition.nextSnapshot?.stateRoots, `${operationName} proof precheck failed: missing next snapshot roots.`);
8502
+ }
8503
+
8504
+ function precheckTokamakExecution({ context, payload, functionProof, aPubBlockHash, operationName }) {
8505
+ expect(payload && typeof payload === "object", `${operationName} proof precheck failed: missing Tokamak payload.`);
8506
+ for (const key of [
8507
+ "proofPart1",
8508
+ "proofPart2",
8509
+ "functionPreprocessPart1",
8510
+ "functionPreprocessPart2",
8511
+ "aPubUser",
8512
+ "aPubBlock",
8513
+ ]) {
8514
+ expect(Array.isArray(payload[key]) && payload[key].length > 0, `${operationName} proof precheck failed: payload.${key} is missing.`);
8515
+ }
8516
+ expect(functionProof?.metadata, `${operationName} proof precheck failed: missing function metadata proof.`);
8517
+ expect(Array.isArray(functionProof?.siblings), `${operationName} proof precheck failed: missing function metadata proof siblings.`);
8518
+ expect(
8519
+ ethers.toBigInt(normalizeBytes32Hex(aPubBlockHash))
8520
+ === ethers.toBigInt(normalizeBytes32Hex(context.workspace.aPubBlockHash)),
8521
+ `${operationName} proof precheck failed: generated aPubBlockHash does not match the channel aPubBlockHash.`,
8522
+ );
8523
+ }
8524
+
8525
+ function contractTxCall(contractMethod, args = [], overrides = undefined, contractInterface = null) {
8526
+ expect(typeof contractMethod === "function", "Internal error: contract transaction method must be callable.");
8527
+ expect(
8528
+ typeof contractMethod.staticCall === "function",
8529
+ "Internal error: contract transaction method must support staticCall.",
8530
+ );
8531
+ const finalArgs = overrides === undefined ? [...args] : [...args, overrides];
8532
+ return {
8533
+ contractInterface,
8534
+ dryRun: () => contractMethod.staticCall(...finalArgs),
8535
+ submit: () => contractMethod(...finalArgs),
8536
+ };
8537
+ }
8538
+
8539
+ async function dryRunThenSubmitTransaction({
8405
8540
  operationName,
8406
- submit,
8541
+ call,
8542
+ precheck = null,
8543
+ context = null,
8544
+ walletName = null,
8545
+ operationDir = null,
8546
+ submittedBefore = [],
8407
8547
  }) {
8548
+ expect(typeof call?.dryRun === "function", "Internal error: transaction dry-run callback is required.");
8549
+ expect(typeof call?.submit === "function", "Internal error: transaction submit callback is required.");
8550
+ if (precheck) {
8551
+ await precheck();
8552
+ }
8408
8553
  try {
8409
- return await waitForReceipt(await submit());
8554
+ await call.dryRun();
8410
8555
  } catch (error) {
8411
- if (isUnexpectedCurrentRootVectorError(error, context)) {
8412
- throw staleChannelRootError({
8413
- cause: error,
8414
- context,
8415
- walletName,
8416
- operationName,
8417
- });
8556
+ throw transactionPreflightOrSubmitError({
8557
+ phase: "dry-run",
8558
+ operationName,
8559
+ cause: error,
8560
+ context,
8561
+ walletName,
8562
+ operationDir,
8563
+ submittedBefore,
8564
+ contractInterface: call.contractInterface,
8565
+ });
8566
+ }
8567
+ try {
8568
+ return await waitForReceipt(await call.submit());
8569
+ } catch (error) {
8570
+ throw transactionPreflightOrSubmitError({
8571
+ phase: "submit",
8572
+ operationName,
8573
+ cause: error,
8574
+ context,
8575
+ walletName,
8576
+ operationDir,
8577
+ submittedBefore,
8578
+ contractInterface: call.contractInterface,
8579
+ });
8580
+ }
8581
+ }
8582
+
8583
+ function transactionPreflightOrSubmitError({
8584
+ phase,
8585
+ operationName,
8586
+ cause,
8587
+ context = null,
8588
+ walletName = null,
8589
+ operationDir = null,
8590
+ submittedBefore = [],
8591
+ contractInterface = null,
8592
+ }) {
8593
+ if (context && isUnexpectedCurrentRootVectorError(cause, context)) {
8594
+ return staleChannelRootError({
8595
+ cause,
8596
+ context,
8597
+ walletName,
8598
+ operationName,
8599
+ phase,
8600
+ });
8601
+ }
8602
+ const decodedError = decodeTransactionContractError(cause, [
8603
+ contractInterface,
8604
+ context?.channelManager?.interface,
8605
+ context?.bridgeTokenVault?.interface,
8606
+ ]);
8607
+ const details = [
8608
+ phase === "dry-run"
8609
+ ? `${operationName} pre-submit dry-run failed. No ${operationName} transaction was submitted.`
8610
+ : `${operationName} transaction submission failed.`,
8611
+ ];
8612
+ const submitted = normalizeSubmittedBefore(submittedBefore);
8613
+ if (submitted.length > 0) {
8614
+ details.push(`Already submitted before this failure: ${submitted.join("; ")}.`);
8615
+ }
8616
+ if (walletName) {
8617
+ details.push(`Wallet: ${walletName}.`);
8618
+ }
8619
+ if (operationDir) {
8620
+ details.push(`Operation directory: ${operationDir}.`);
8621
+ }
8622
+ if (decodedError) {
8623
+ details.push(`Decoded contract error: ${decodedError}.`);
8624
+ }
8625
+ details.push(`Provider error: ${extractProviderErrorMessage(cause)}.`);
8626
+ const error = cliError(
8627
+ phase === "dry-run" ? CLI_ERROR_CODES.TX_DRY_RUN_FAILED : CLI_ERROR_CODES.TX_SUBMIT_FAILED,
8628
+ details.join(" "),
8629
+ { cause },
8630
+ );
8631
+ error.phase = phase;
8632
+ error.operationName = operationName;
8633
+ error.transactionSubmitted = phase !== "dry-run";
8634
+ error.submittedBefore = submitted;
8635
+ error.walletName = walletName;
8636
+ error.operationDir = operationDir;
8637
+ error.decodedContractError = decodedError;
8638
+ error.providerError = extractProviderErrorMessage(cause);
8639
+ if (context) {
8640
+ error.channelName = context.workspace?.channelName;
8641
+ error.networkName = context.workspace?.network;
8642
+ }
8643
+ return error;
8644
+ }
8645
+
8646
+ function submittedReceiptSummary(label, receipt) {
8647
+ return `${label} tx ${receipt?.hash ?? "<unknown>"} in block ${receipt?.blockNumber ?? "<unknown>"}`;
8648
+ }
8649
+
8650
+ function normalizeSubmittedBefore(entries) {
8651
+ return entries
8652
+ .filter(Boolean)
8653
+ .map((entry) => {
8654
+ if (typeof entry === "string") {
8655
+ return entry;
8656
+ }
8657
+ if (entry?.hash) {
8658
+ return submittedReceiptSummary(entry.label ?? "transaction", entry);
8659
+ }
8660
+ return String(entry);
8661
+ });
8662
+ }
8663
+
8664
+ function decodeTransactionContractError(error, contractInterfaces) {
8665
+ if (error?.revert?.name) {
8666
+ return formatDecodedContractError(error.revert.name, error.revert.args ?? []);
8667
+ }
8668
+ for (const contractInterface of contractInterfaces.filter(Boolean)) {
8669
+ for (const errorData of extractContractErrorDataCandidates(error)) {
8670
+ try {
8671
+ const parsed = contractInterface.parseError(errorData);
8672
+ if (parsed) {
8673
+ return formatDecodedContractError(parsed.name, parsed.args ?? []);
8674
+ }
8675
+ } catch {
8676
+ // Keep scanning provider error payloads and interfaces.
8677
+ }
8418
8678
  }
8419
- throw error;
8420
8679
  }
8680
+ return null;
8681
+ }
8682
+
8683
+ function formatDecodedContractError(name, args) {
8684
+ const renderedArgs = Array.from(args ?? [])
8685
+ .map((value) => serializeBigInts(normalizeCliOutputValue(value, [])));
8686
+ return `${name}(${renderedArgs.map((value) => JSON.stringify(value)).join(", ")})`;
8687
+ }
8688
+
8689
+ function extractProviderErrorMessage(error) {
8690
+ return String(
8691
+ error?.shortMessage
8692
+ ?? error?.reason
8693
+ ?? error?.info?.error?.message
8694
+ ?? error?.error?.message
8695
+ ?? error?.message
8696
+ ?? error,
8697
+ );
8421
8698
  }
8422
8699
 
8423
8700
  function staleChannelRootError({
@@ -8425,17 +8702,24 @@ function staleChannelRootError({
8425
8702
  context,
8426
8703
  walletName,
8427
8704
  operationName,
8705
+ phase = "submit",
8428
8706
  }) {
8429
8707
  const message = [
8430
- `${operationName} failed because the submitted proof was generated for an older channel root.`,
8708
+ phase === "dry-run"
8709
+ ? `${operationName} pre-submit dry-run failed because the generated proof targets an older channel root. No ${operationName} transaction was submitted.`
8710
+ : `${operationName} failed because the submitted proof was generated for an older channel root.`,
8431
8711
  "The rejected proof cannot be reused.",
8432
8712
  "Do not change recipients, amounts, note counts, function arity, or split the command as recovery.",
8433
8713
  "Refresh the channel workspace, re-check affected wallet state when the command uses notes, then rerun the original intended command so the CLI regenerates a proof from a fresh snapshot.",
8434
8714
  ].join(" ");
8435
8715
  const error = cliError(CLI_ERROR_CODES.STALE_CHANNEL_ROOT, message, { cause });
8716
+ error.phase = phase;
8717
+ error.operationName = operationName;
8718
+ error.transactionSubmitted = phase !== "dry-run";
8436
8719
  error.channelName = context.workspace.channelName;
8437
8720
  error.networkName = context.workspace.network;
8438
8721
  error.walletName = walletName;
8722
+ error.providerError = extractProviderErrorMessage(cause);
8439
8723
  error.retryPolicy = "recover_workspace_then_regenerate_proof";
8440
8724
  error.semanticMutationAllowed = false;
8441
8725
  error.reuseProofAllowed = false;
@@ -9742,15 +10026,11 @@ function assertVersionArgs(args) {
9742
10026
  }
9743
10027
 
9744
10028
  function printVersion() {
9745
- if (isJsonOutputRequested()) {
9746
- printJson({
9747
- action: "version",
9748
- packageName: privateStateCliPackageJson.name,
9749
- version: privateStateCliPackageJson.version,
9750
- });
9751
- return;
9752
- }
9753
- console.log(privateStateCliPackageJson.version);
10029
+ cliOutput.result({
10030
+ action: "version",
10031
+ packageName: privateStateCliPackageJson.name,
10032
+ version: privateStateCliPackageJson.version,
10033
+ });
9754
10034
  }
9755
10035
 
9756
10036
  function requireWorkspaceName(args) {
@@ -10795,38 +11075,53 @@ function buildWalletViewingKeyMetadata(wallet) {
10795
11075
  }
10796
11076
 
10797
11077
  function printHelp() {
10798
- const commandHelp = PRIVATE_STATE_CLI_COMMANDS.map((command) => [
10799
- ` ${privateStateCliCommandSynopsis(command)}`,
10800
- ` ${command.description}`,
10801
- ...(command.help ?? []).map((line) => ` ${line}`),
10802
- ].join("\n")).join("\n\n");
10803
- console.log(`
10804
- Commands:
10805
- ${commandHelp}
10806
-
10807
- Secret source options:
10808
- Use account import --private-key-file once to create a protected local account secret.
10809
- L1 signing commands use --account only.
10810
- A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
10811
- Create one before joining a channel, for example:
10812
- openssl rand -hex 32 > ./wallet-secret.txt
10813
- private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt --acknowledge-action-impact
10814
- Configure each network RPC endpoint once with set rpc. The CLI reads RPC_URL, LOG_CHUNK_SIZE,
10815
- and LOG_REQUESTS_PER_SECOND from ~/tokamak-private-channels/workspace/<network>/rpc-config.env.
10816
- Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.
10817
- Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but
10818
- canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
10819
-
10820
- Options:
10821
- --version
10822
- Print the private-state CLI package version and exit.
11078
+ cliOutput.result(buildHelpCommandsResult());
11079
+ }
10823
11080
 
10824
- --json
10825
- Print the command result as JSON. Without --json, commands print human-readable output.
11081
+ function buildHelpCommandsResult() {
11082
+ return {
11083
+ action: "help commands",
11084
+ commands: PRIVATE_STATE_CLI_COMMANDS.map(buildHelpCommandEntry),
11085
+ secretSourceOptions: [
11086
+ "Use account import --private-key-file once to create a protected local account secret.",
11087
+ "L1 signing commands use --account only.",
11088
+ "A wallet secret source file is arbitrary high-entropy secret text read once by channel join.",
11089
+ "Configure each network RPC endpoint once with set rpc.",
11090
+ "Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.",
11091
+ "Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but canonical CLI secret files remain protected.",
11092
+ ],
11093
+ globalOptions: [
11094
+ {
11095
+ option: "--version",
11096
+ description: "Print the private-state CLI package version and exit.",
11097
+ },
11098
+ {
11099
+ option: "--json",
11100
+ description: "Print the final success or failure result as JSON on stdout. Progress, warning, and info events are JSONL on stderr.",
11101
+ },
11102
+ {
11103
+ option: "--help",
11104
+ description: "Show this help. Equivalent to help commands.",
11105
+ },
11106
+ ],
11107
+ };
11108
+ }
10826
11109
 
10827
- --help
10828
- Show this help. Equivalent to help commands.
10829
- `);
11110
+ function buildHelpCommandEntry(command) {
11111
+ const requiredFields = privateStateCliCommandRequiredOptionKeys(command);
11112
+ const fields = command.fields ?? [];
11113
+ return {
11114
+ id: command.id,
11115
+ display: privateStateCliCommandDisplay(command),
11116
+ synopsis: privateStateCliCommandSynopsis(command),
11117
+ description: command.description,
11118
+ usage: command.usage,
11119
+ fields,
11120
+ requiredFields,
11121
+ optionalFields: fields.filter((field) => !requiredFields.includes(field)),
11122
+ installMode: privateStateCliCommandInstallMode(command),
11123
+ help: command.help ?? [],
11124
+ };
10830
11125
  }
10831
11126
 
10832
11127
  function readJson(filePath) {
@@ -11350,29 +11645,122 @@ function loadWalletCommandRuntime(args, { prepareArtifacts = false } = {}) {
11350
11645
  }
11351
11646
 
11352
11647
  const HUMAN_RESULT_RENDERERS = Object.freeze({
11648
+ doctor: printDoctorHumanReport,
11353
11649
  guide: printGuideHumanResult,
11650
+ "help commands": printHelpCommandsHumanResult,
11354
11651
  investigator: printInvestigatorHumanResult,
11355
11652
  observer: printObserverHumanResult,
11356
11653
  "transaction-fees": printTransactionFeesHumanResult,
11357
11654
  update: printUpdateHumanResult,
11655
+ version: printVersionHumanResult,
11358
11656
  });
11359
11657
 
11360
11658
  function normalizePrivateKey(value) {
11361
11659
  return value.startsWith("0x") ? value : `0x${value}`;
11362
11660
  }
11363
11661
 
11364
- function printJson(value) {
11365
- const normalized = normalizeCliOutput(value);
11662
+ const cliOutput = Object.freeze({
11663
+ result(value) {
11664
+ const normalized = normalizeCliOutput(value);
11665
+ if (isJsonOutputRequested()) {
11666
+ console.log(JSON.stringify(buildJsonSuccessPayload(normalized), null, 2));
11667
+ return;
11668
+ }
11669
+ const renderer = HUMAN_RESULT_RENDERERS[normalized?.action];
11670
+ if (renderer) {
11671
+ renderer(normalized);
11672
+ return;
11673
+ }
11674
+ printHumanResult(normalized);
11675
+ },
11676
+ error(error, args = {}) {
11677
+ if (isJsonOutputRequested()) {
11678
+ console.log(JSON.stringify(normalizeCliOutput(buildJsonErrorPayload(error, args)), null, 2));
11679
+ return;
11680
+ }
11681
+ console.error(formatHumanError(error, args));
11682
+ },
11683
+ progress(action, phase, details = {}) {
11684
+ emitOutputEvent({
11685
+ event: "progress",
11686
+ action,
11687
+ phase,
11688
+ message: details.message ?? `[${action}] ${phase}`,
11689
+ details,
11690
+ });
11691
+ },
11692
+ warning(kind, message, details = {}) {
11693
+ emitOutputEvent({
11694
+ event: "warning",
11695
+ kind,
11696
+ message,
11697
+ details,
11698
+ });
11699
+ },
11700
+ });
11701
+
11702
+ function buildJsonSuccessPayload(value) {
11703
+ if (value && typeof value === "object" && !Array.isArray(value)) {
11704
+ return Object.hasOwn(value, "ok") ? value : { ok: true, ...value };
11705
+ }
11706
+ return {
11707
+ ok: true,
11708
+ action: "result",
11709
+ value,
11710
+ };
11711
+ }
11712
+
11713
+ function emitOutputEvent(event) {
11714
+ const normalized = normalizeCliOutput({
11715
+ timestamp: new Date().toISOString(),
11716
+ ...event,
11717
+ });
11366
11718
  if (isJsonOutputRequested()) {
11367
- console.log(JSON.stringify(normalized, null, 2));
11719
+ console.error(JSON.stringify(normalized));
11368
11720
  return;
11369
11721
  }
11370
- const renderer = HUMAN_RESULT_RENDERERS[normalized?.action];
11371
- if (renderer) {
11372
- renderer(normalized);
11722
+ const message = event.message ?? `[${event.action ?? event.kind ?? "cli"}] ${event.phase ?? event.event}`;
11723
+ if (event.event === "warning") {
11724
+ console.error(message);
11373
11725
  return;
11374
11726
  }
11375
- printHumanResult(normalized);
11727
+ console.log(message);
11728
+ }
11729
+
11730
+ function printVersionHumanResult(result) {
11731
+ console.log(result.version);
11732
+ }
11733
+
11734
+ function printHelpCommandsHumanResult(help) {
11735
+ const commandHelp = (help.commands ?? []).map((command) => [
11736
+ ` ${command.synopsis}`,
11737
+ ` ${command.description}`,
11738
+ ...(command.help ?? []).map((line) => ` ${line}`),
11739
+ ].join("\n")).join("\n\n");
11740
+ const globalOptions = (help.globalOptions ?? []).map((option) => [
11741
+ ` ${option.option}`,
11742
+ ` ${option.description}`,
11743
+ ].join("\n")).join("\n\n");
11744
+ console.log(`
11745
+ Commands:
11746
+ ${commandHelp}
11747
+
11748
+ Secret source options:
11749
+ Use account import --private-key-file once to create a protected local account secret.
11750
+ L1 signing commands use --account only.
11751
+ A wallet secret source file is arbitrary high-entropy secret text read once by channel join.
11752
+ Create one before joining a channel, for example:
11753
+ openssl rand -hex 32 > ./wallet-secret.txt
11754
+ private-state-cli channel join --channel-name <NAME> --network <NAME> --account <NAME> --wallet-secret-path ./wallet-secret.txt --acknowledge-action-impact
11755
+ Configure each network RPC endpoint once with set rpc. The CLI reads RPC_URL, LOG_CHUNK_SIZE,
11756
+ and LOG_REQUESTS_PER_SECOND from ~/tokamak-private-channels/workspace/<network>/rpc-config.env.
11757
+ Wallet commands use separate protected viewing-key and spending-key files when those capabilities are needed.
11758
+ Source files passed to --private-key-file and --wallet-secret-path are not required to use 0600 permissions, but
11759
+ canonical CLI secret files remain protected. On macOS/Linux this means 0600; on Windows the CLI repairs ACLs when possible.
11760
+
11761
+ Options:
11762
+ ${globalOptions}
11763
+ `);
11376
11764
  }
11377
11765
 
11378
11766
  function printGuideHumanResult(guide) {
@@ -11604,12 +11992,7 @@ function humanizeLabel(value) {
11604
11992
  }
11605
11993
 
11606
11994
  function emitProgress(action, phase) {
11607
- const line = `[${action}] ${phase}`;
11608
- if (isJsonOutputRequested()) {
11609
- console.error(line);
11610
- } else {
11611
- console.log(line);
11612
- }
11995
+ cliOutput.progress(action, phase);
11613
11996
  }
11614
11997
 
11615
11998
  function createByteDownloadProgress({ action, label, url }) {
@@ -11745,7 +12128,7 @@ function createRpcLogScanProgress({ action, label }) {
11745
12128
  };
11746
12129
  }
11747
12130
 
11748
- function formatCliErrorForDisplay(error, args = {}) {
12131
+ function formatHumanError(error, args = {}) {
11749
12132
  const message = String(error?.message ?? error);
11750
12133
  const hints = buildRecoveryHints(error, args);
11751
12134
  if (hints.length === 0) {
@@ -11758,6 +12141,41 @@ function formatCliErrorForDisplay(error, args = {}) {
11758
12141
  ].join("\n");
11759
12142
  }
11760
12143
 
12144
+ function buildJsonErrorPayload(error, args = {}) {
12145
+ const message = String(error?.message ?? error);
12146
+ const hints = buildRecoveryHints(error, args);
12147
+ return {
12148
+ ok: false,
12149
+ action: "error",
12150
+ error: {
12151
+ name: error?.name ?? "Error",
12152
+ code: error?.code ?? "ERROR",
12153
+ command: typeof args.command === "string" ? args.command : null,
12154
+ message,
12155
+ hints,
12156
+ phase: error?.phase ?? null,
12157
+ operationName: error?.operationName ?? null,
12158
+ transactionSubmitted: typeof error?.transactionSubmitted === "boolean"
12159
+ ? error.transactionSubmitted
12160
+ : null,
12161
+ submittedBefore: Array.isArray(error?.submittedBefore) ? error.submittedBefore : [],
12162
+ decodedContractError: error?.decodedContractError ?? null,
12163
+ providerError: error?.providerError ?? null,
12164
+ channelName: error?.channelName ?? (typeof args.channelName === "string" ? args.channelName : null),
12165
+ networkName: error?.networkName ?? (typeof args.network === "string" ? args.network : null),
12166
+ walletName: error?.walletName ?? (typeof args.wallet === "string" ? args.wallet : null),
12167
+ operationDir: error?.operationDir ?? null,
12168
+ retryPolicy: error?.retryPolicy ?? null,
12169
+ semanticMutationAllowed: typeof error?.semanticMutationAllowed === "boolean"
12170
+ ? error.semanticMutationAllowed
12171
+ : null,
12172
+ reuseProofAllowed: typeof error?.reuseProofAllowed === "boolean"
12173
+ ? error.reuseProofAllowed
12174
+ : null,
12175
+ },
12176
+ };
12177
+ }
12178
+
11761
12179
  function buildRecoveryHints(error, args = {}) {
11762
12180
  const message = String(error?.message ?? error);
11763
12181
  const hints = [];
@@ -11950,5 +12368,5 @@ export {
11950
12368
  loadExplicitCommandRuntime,
11951
12369
  loadWalletCommandRuntime,
11952
12370
  assertProviderChainIdMatchesNetwork,
11953
- formatCliErrorForDisplay,
12371
+ cliOutput,
11954
12372
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamak-private-dapps/private-state-cli",
3
- "version": "2.4.2",
3
+ "version": "2.4.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",